diff --git a/demos/python/sdk_wireless_camera_control/docs/changelog.rst b/demos/python/sdk_wireless_camera_control/docs/changelog.rst index 3c61987e..3929587f 100644 --- a/demos/python/sdk_wireless_camera_control/docs/changelog.rst +++ b/demos/python/sdk_wireless_camera_control/docs/changelog.rst @@ -10,7 +10,7 @@ The format is based on `Keep a Changelog ` and this project adheres to `Semantic Versioning `_. 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..0b8bc436 100644 --- a/demos/python/sdk_wireless_camera_control/noxfile.py +++ b/demos/python/sdk_wireless_camera_control/noxfile.py @@ -78,6 +78,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..ef4462f0 100644 --- a/demos/python/sdk_wireless_camera_control/pyproject.toml +++ b/demos/python/sdk_wireless_camera_control/pyproject.toml @@ -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()