diff --git a/.github/workflows/python_sdk_test.yml b/.github/workflows/python_sdk_test.yml
index d462f59f..f81e4d2b 100644
--- a/.github/workflows/python_sdk_test.yml
+++ b/.github/workflows/python_sdk_test.yml
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
- python-version: ["3.9", "3.10", "3.11"]
+ python-version: ['3.9', '3.10', '3.11']
include:
- os: ubuntu-latest
path: ~/.cache/pip
@@ -41,8 +41,8 @@ jobs:
key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('demos/python/sdk_wireless_camera_control/poetry.lock') }}
restore-keys: ${{ runner.os }}-pip-
- - name: Install Bluez on Ubuntu
- run: sudo apt-get update && sudo apt install -y bluez
+ - name: Install prereqs on Ubuntu
+ run: sudo apt-get update && sudo apt install -y bluez graphviz
if: ${{ matrix.os == 'ubuntu-latest'}}
- name: Install dependencies
@@ -53,10 +53,16 @@ jobs:
pip install nox-poetry==1.0.3
pip install poetry
- - name: Perform checks (format, types, lint, docstrings, unit tests, docs, safety)
+ - name: Perform checks (format, types, lint, docstrings, unit tests)
working-directory: ./demos/python/sdk_wireless_camera_control/
run: nox -p ${{ matrix.python-version }}
+ # Only test docs with latest Python on ubuntu since we need graphviz
+ - name: Test docs build
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
+ working-directory: ./demos/python/sdk_wireless_camera_control/
+ run: nox -s docs
+
- name: Archive test report on failure
uses: actions/upload-artifact@v2
if: failure()
diff --git a/demos/python/sdk_wireless_camera_control/docs/changelog.rst b/demos/python/sdk_wireless_camera_control/docs/changelog.rst
index 3c61987e..d34046d0 100644
--- a/demos/python/sdk_wireless_camera_control/docs/changelog.rst
+++ b/demos/python/sdk_wireless_camera_control/docs/changelog.rst
@@ -9,8 +9,12 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog `_,
and this project adheres to `Semantic Versioning `_.
+Unreleased
+----------
+* Fix BLE notifications not being routed correctly
+
0.14.0 (September-13-2022)
--------------------------
+--------------------------
* NOTE! This is a major update and includes massive API breaking changes.
* Move to asyncio-based framework
* Add HERO 12 support
diff --git a/demos/python/sdk_wireless_camera_control/noxfile.py b/demos/python/sdk_wireless_camera_control/noxfile.py
index 73b1e430..4c9bb5e1 100644
--- a/demos/python/sdk_wireless_camera_control/noxfile.py
+++ b/demos/python/sdk_wireless_camera_control/noxfile.py
@@ -6,7 +6,8 @@
import nox
from nox_poetry import session
-nox.options.sessions = "format", "lint", "tests", "docstrings", "docs"
+# Don't run docs by default since it needs graphviz.
+nox.options.sessions = "format", "lint", "tests", "docstrings"
SUPPORTED_VERSIONS = [
"3.9",
@@ -78,6 +79,6 @@ def docs(session) -> None:
"autodoc-pydantic",
"darglint",
)
- session.run("sphinx-build", "docs", "docs/build")
+ session.run("sphinx-build", "-W", "docs", "docs/build")
# Clean up for Jekyll consumption
session.run("rm", "-rf", "docs/build/.doctrees", "/docs/build/_sources", "/docs/build/_static/fonts", external=True)
diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/log_battery.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/log_battery.py
index 78cb29fa..cf3117d9 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
@@ -17,7 +17,7 @@
from open_gopro import WirelessGoPro, types
from open_gopro.constants import StatusId
from open_gopro.logger import set_stream_logging_level, setup_logging
-from open_gopro.util import add_cli_args_and_parse
+from open_gopro.util import add_cli_args_and_parse, ainput
console = Console()
@@ -84,56 +84,55 @@ async def process_battery_notifications(update: types.UpdateType, value: int) ->
async def main(args: argparse.Namespace) -> None:
logger = setup_logging(__name__, args.log)
- global SAMPLE_INDEX
gopro: Optional[WirelessGoPro] = None
try:
async with WirelessGoPro(args.identifier, enable_wifi=False) as gopro:
set_stream_logging_level(logging.ERROR)
- if args.poll:
- with console.status("[bold green]Polling the battery until it dies..."):
- while True:
- SAMPLES.append(
- Sample(
- index=SAMPLE_INDEX,
- percentage=(await gopro.ble_status.int_batt_per.get_value()).data,
- bars=(await gopro.ble_status.batt_level.get_value()).data,
+ async def log_battery() -> None:
+ global SAMPLE_INDEX
+ if args.poll:
+ with console.status("[bold green]Polling the battery until it dies..."):
+ while True:
+ SAMPLES.append(
+ Sample(
+ index=SAMPLE_INDEX,
+ percentage=(await gopro.ble_status.int_batt_per.get_value()).data,
+ bars=(await gopro.ble_status.batt_level.get_value()).data,
+ )
)
- )
- console.print(str(SAMPLES[-1]))
- SAMPLE_INDEX += 1
- await asyncio.sleep(args.poll)
- # Otherwise set up notifications
- else:
- global last_bars
- global last_percentage
-
- console.print("Configuring battery notifications...")
- # Enable notifications of the relevant battery statuses. Also store initial values.
- last_bars = (
- await gopro.ble_status.batt_level.register_value_update(process_battery_notifications)
- ).data
- last_percentage = (
- await gopro.ble_status.int_batt_per.register_value_update(process_battery_notifications)
- ).data
- # Append initial sample
- SAMPLES.append(Sample(index=SAMPLE_INDEX, percentage=last_percentage, bars=last_bars))
- console.print(str(SAMPLES[-1]))
-
- # Start a thread to handle asynchronous battery level notifications
- console.print("[bold green]Receiving battery notifications until it dies...")
- while True:
- await asyncio.sleep(1)
+ console.print(str(SAMPLES[-1]))
+ SAMPLE_INDEX += 1
+ await asyncio.sleep(args.poll)
+ else: # Not polling. Set up notifications
+ global last_bars
+ global last_percentage
+
+ console.print("Configuring battery notifications...")
+ # Enable notifications of the relevant battery statuses. Also store initial values.
+ last_bars = (
+ await gopro.ble_status.batt_level.register_value_update(process_battery_notifications)
+ ).data
+ last_percentage = (
+ await gopro.ble_status.int_batt_per.register_value_update(process_battery_notifications)
+ ).data
+ # Append initial sample
+ SAMPLES.append(Sample(index=SAMPLE_INDEX, percentage=last_percentage, bars=last_bars))
+ console.print(str(SAMPLES[-1]))
+ console.print("[bold green]Receiving battery notifications until it dies...")
+
+ asyncio.create_task(log_battery())
+ await ainput("[purple]Press enter to exit.", console.print)
+ console.print("Exiting...")
except KeyboardInterrupt:
logger.warning("Received keyboard interrupt. Shutting down...")
- if len(SAMPLES) > 0:
+ if SAMPLES:
csv_location = Path(args.log.parent) / "battery_results.csv"
dump_results_as_csv(csv_location)
if gopro:
await gopro.close()
- console.print("Exiting...")
def parse_arguments() -> argparse.Namespace:
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 2ec35932..a10c8dfc 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
@@ -27,7 +27,7 @@
)
from open_gopro.ble import BleakWrapperController, BleUUID
from open_gopro.communicator_interface import GoProWirelessInterface, MessageRules
-from open_gopro.constants import ActionId, GoProUUIDs, QueryCmdId, SettingId, StatusId
+from open_gopro.constants import ActionId, GoProUUIDs, StatusId
from open_gopro.gopro_base import GoProBase, GoProMessageInterface, MessageMethodType
from open_gopro.logger import Logger
from open_gopro.models.response import BleRespBuilder, GoProResp
@@ -538,12 +538,17 @@ async def _update_internal_state(self, update: types.UpdateType, value: int) ->
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
"""
+ # Flatten data if possible
+ if response._is_query and not response._is_push:
+ response.data = list(response.data.values())[0]
+
# Check if this is the awaited synchronous response (id matches). Note! these have to come in order.
response_claimed = False
if await self._sync_resp_wait_q.peek_front() == response.identifier:
@@ -554,10 +559,10 @@ async def _route_response(self, response: GoProResp) -> None:
# If this wasn't the awaited synchronous response...
if not response_claimed:
logger.info(Logger.build_log_rx_str(response, asynchronous=True))
- if isinstance(response.identifier, QueryCmdId):
+ if response._is_push:
for update_id, value in response.data.items():
await self._notify_listeners(update_id, value)
- elif isinstance(response.identifier, (StatusId, SettingId, ActionId)):
+ elif isinstance(response.identifier, ActionId):
await self._notify_listeners(response.identifier, response.data)
def _notification_handler(self, handle: int, data: bytearray) -> None:
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 df15512e..748a1a91 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
@@ -143,6 +143,28 @@ def ok(self) -> bool:
"""
return self.status in [ErrorCode.SUCCESS, ErrorCode.UNKNOWN]
+ @property
+ def _is_push(self) -> bool:
+ """Was this response an asynchronous push?
+
+ Returns:
+ bool: True if yes, False otherwise
+ """
+ return self.identifier in [
+ QueryCmdId.STATUS_VAL_PUSH,
+ QueryCmdId.SETTING_VAL_PUSH,
+ QueryCmdId.SETTING_CAPABILITY_PUSH,
+ ]
+
+ @property
+ def _is_query(self) -> bool:
+ """Is this response to a settings / status query?
+
+ Returns:
+ bool: True if yes, False otherwise
+ """
+ return isinstance(self.identifier, QueryCmdId)
+
class RespBuilder(ABC, Generic[T]):
"""Common Response Builder Interface"""
@@ -437,12 +459,6 @@ def build(self) -> GoProResp:
# Query (setting get value, status get value, etc.)
if query_type:
- is_multiple = self._identifier in [
- QueryCmdId.GET_CAPABILITIES_VAL,
- QueryCmdId.REG_CAPABILITIES_UPDATE,
- QueryCmdId.SETTING_CAPABILITY_PUSH,
- ]
-
camera_state: types.CameraState = defaultdict(list)
self._status = ErrorCode(buf[0])
buf = buf[1:]
@@ -467,7 +483,11 @@ def build(self) -> GoProResp:
camera_state[param_id] = param_val
continue
# These can be more than 1 value so use a list
- if is_multiple:
+ if self._identifier in [
+ QueryCmdId.GET_CAPABILITIES_VAL,
+ QueryCmdId.REG_CAPABILITIES_UPDATE,
+ QueryCmdId.SETTING_CAPABILITY_PUSH,
+ ]:
# Parse using parser from global map and append
camera_state[param_id].append(parser.parse(param_val))
else:
@@ -479,12 +499,8 @@ def build(self) -> GoProResp:
# isn't functionally critical
logger.warning(f"{param_id} does not contain a value {param_val}")
camera_state[param_id] = param_val
- # Flatten if not multiple
- if is_multiple:
- self._identifier = list(camera_state.keys())[0]
- parsed = list(camera_state.values())[0]
- else:
- parsed = camera_state
+ parsed = camera_state
+
else: # Commands, Protobuf, and direct Reads
if is_cmd := isinstance(self._identifier, CmdId):
# All (non-protobuf) commands have a status
diff --git a/demos/python/sdk_wireless_camera_control/pyproject.toml b/demos/python/sdk_wireless_camera_control/pyproject.toml
index 9c682ce8..dfc1b184 100644
--- a/demos/python/sdk_wireless_camera_control/pyproject.toml
+++ b/demos/python/sdk_wireless_camera_control/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "open_gopro"
-version = "0.14.0"
+version = "0.14.1"
description = "Open GoPro API and Examples"
authors = ["Tim Camise "]
readme = "README.md"
@@ -183,7 +183,7 @@ log_file_level = "DEBUG"
log_file_format = "%(threadName)13s: %(name)40s:%(lineno)5d %(asctime)s.%(msecs)03d %(levelname)-8s | %(message)s"
log_file_date_format = "%H:%M:%S"
filterwarnings = "ignore::DeprecationWarning"
-# timeout = 10
+timeout = 10
addopts = [
"-s",
"--capture=tee-sys",
diff --git a/demos/python/sdk_wireless_camera_control/tests/conftest.py b/demos/python/sdk_wireless_camera_control/tests/conftest.py
index 75618167..10738c98 100644
--- a/demos/python/sdk_wireless_camera_control/tests/conftest.py
+++ b/demos/python/sdk_wireless_camera_control/tests/conftest.py
@@ -40,6 +40,7 @@
from open_gopro.communicator_interface import GoProBle, GoProWifi
from open_gopro.constants import CmdId, ErrorCode, GoProUUIDs, StatusId
from open_gopro.exceptions import ConnectFailed, FailedToFindDevice
+from open_gopro.gopro_base import GoProBase
from open_gopro.logger import set_logging_level, setup_logging
from open_gopro.models.response import GoProResp
from open_gopro.wifi import SsidState, WifiClient, WifiController
@@ -444,6 +445,7 @@ def close(self) -> None:
@pytest.fixture(params=versions)
async def mock_wireless_gopro_basic(request):
test_client = MockWirelessGoPro(request.param)
+ GoProBase.HTTP_GET_RETRIES = 1
yield test_client
test_client.close()
diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py
index f76b5259..d9920de8 100644
--- a/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py
+++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py
@@ -35,8 +35,10 @@ def test_push_response_no_parameter_values():
assert builder.is_finished_accumulating
r = builder.build()
assert r.ok
- assert r.identifier == SettingId.RESOLUTION
- assert r.data == []
+ assert r.identifier == QueryCmdId.SETTING_CAPABILITY_PUSH
+ assert r.data[SettingId.RESOLUTION] == []
+ assert r.data[SettingId.FPS] == []
+ assert r.data[SettingId.VIDEO_FOV] == []
test_read_receive = bytearray([0x64, 0x62, 0x32, 0x2D, 0x73, 0x58, 0x56, 0x2D, 0x66, 0x62, 0x38])
diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py
index 5b04e2e2..b56269fc 100644
--- a/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py
+++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py
@@ -131,7 +131,7 @@ async def receive_encoding_status(id: types.UpdateType, value: bool):
event.set()
mock_wireless_gopro_basic.register_update(receive_encoding_status, StatusId.ENCODING)
- not_encoding = bytearray([0x05, 0x13, 0x00, StatusId.ENCODING.value, 0x01, 0x00])
+ not_encoding = bytearray([0x05, 0x93, 0x00, StatusId.ENCODING.value, 0x01, 0x00])
mock_wireless_gopro_basic._notification_handler(0xFF, not_encoding)
await event.wait()
@@ -153,7 +153,7 @@ async def receive_encoding_status(id: types.UpdateType, value: bool):
event.set()
mock_wireless_gopro_basic.register_update(receive_encoding_status, StatusId.ENCODING)
- not_encoding = bytearray([0x05, 0x13, 0x00, StatusId.ENCODING.value, 0x01, 0x00])
+ not_encoding = bytearray([0x05, 0x93, 0x00, StatusId.ENCODING.value, 0x01, 0x00])
mock_wireless_gopro_basic._notification_handler(0xFF, not_encoding)
await event.wait()