From 9d3c4f80e5c0f1c7484e19b874ec65a26f97e34b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:51:54 +0000 Subject: [PATCH] Built Open GoPro docs from Internal Repo --- assets/js/lunr/lunr-store-base.js | 6 +- assets/js/lunr/lunr-store.js | 6 +- contribution.html | 34 +++---- faq.html | 9 ++ feed.xml | 2 +- sitemap.xml | 22 ++--- tutorials/ble-protobuf.html | 38 ++++---- tutorials/ble-queries.html | 138 ++++++++++++++--------------- tutorials/camera-media-list.html | 50 +++++------ tutorials/cohn.html | 38 ++++---- tutorials/connect-ble.html | 64 ++++++------- tutorials/connect-wifi.html | 96 ++++++++++---------- tutorials/parse-ble-responses.html | 138 ++++++++++++++--------------- tutorials/send-ble-commands.html | 100 ++++++++++----------- tutorials/send-wifi-commands.html | 100 ++++++++++----------- 15 files changed, 425 insertions(+), 416 deletions(-) diff --git a/assets/js/lunr/lunr-store-base.js b/assets/js/lunr/lunr-store-base.js index 8258c6a5..ff3c9f3d 100644 --- a/assets/js/lunr/lunr-store-base.js +++ b/assets/js/lunr/lunr-store-base.js @@ -1,7 +1,7 @@ var jekyllStore = [ { "title": "FAQ and Known Issues: ", - "excerpt": "Frequently Asked Questions (FAQ) If you have somehow stumbled here first, note that there are specifications, demos, and tutorials which expand upon much of the information here. These can be found, among other places, from the home page. Connectivity What is the distance from the camera that BLE will still work? It is standard Bluetooth 4.0 range and it depends on external factors such as: Interference: anything interfering with the signal will shorten the range. The type of device that the camera is connected to: BT classification distinguishes 3 device classes based on their power levels. Depending on the class of the connected device, the range varies from less than 10 meters to 100 meters. Can I connect using WiFi only? Theoretically yes, if you already know the SSID, password, and the camera’s WiFi AP has been enabled. However, practically no because BLE is required in order to discover this information and configure the AP. Can I connect using BLE only? Yes, however there is some functionality that is not possible over BLE such as accessing the media list and downloading files. How to allow third-party devices to automatically discover close-by GoPro cameras? Devices can only be discovered via BLE by scanning for advertising GoPro cameras Multi Camera Setups How many devices can connect to the camera? Simultaneously, only one device can connect at a time. However, the camera stores BLE security keys and other connection information so it is possible to connect multiple devices sequentially. Is there currently a way to connect multiple cameras on the same Wifi network? No. Cameras can only be connected through Wi-Fi by becoming an access point itself (generating its own Wi-Fi network), not as a peripheral. What is the time offset between multiple cameras executing the same command? In cases when camera sync is important, we recommend using the USB connection, which minimizes the variance among devices. The time drift among cameras connected by USB cable to the same host will be up to ~35ms. Using BLE for that purpose will further increase it. Is there a way to precisely time sync cameras so the footage can be aligned during post-processing? The cameras set their time via GPS. By default, the camera seeks GPS at the beginning of every session, but this can be hindered by typical limitations of GPS signals. Additionally, there are two advanced options that require GoPro Labs firmware installed on the camera. The best bet is multi-cam GPS sync. Another option is precise time calibration via a dynamic QR scan from a smartphone or PC. Streaming What are the differences between the streaming options for GoPros? There are currently 3 different options on how to stream out of GoPro cameras. They are available either via Wi-Fi, USB, or both.   : Wifi :   : USB :       ViewFinder Preview LiveStream ViewFinder Preview Webcam Preview Webcam Orientation Floating or Fixed Landscape facing up Floating or Fixed Landscape: Floating or Fixed Landscape: Floating or Fixed Streaming Protocols UDP (MPEG-TS) RTMP UDP (MPEG-TS) UDP (MPEG-TS) UDP (MPEG-TS) \\         RTSP RTSP Connection Protocol Wifi - AP Mode WiFi - STA Mode NCM NCM NCM Resolution 480p, 720p 480p, 720p, 1080p 480p, 720p 720p, 1080p 720p, 1080p Frame Rate 30 30 30 30 30 Bitrate 2.5 - 4 mbps 0.8 - 8 mbps 2.5 - 4 mbps 6 mbps 6 mbps \\   depending on model configurable depending on model     Stabilization Basic Stabilization HyperSmooth or none Basic Stabilization None None Image Quality Basic Same as recorded content Basic Basic Same as recorded content Minimum Latency 210 ms > 100ms (un-stabilized) 210 ms 210 ms 210 ms \\     > 1,100ms (stabilized)       Audio None Stereo None None None Max Stream Time 150 minutes (720p on fully 85 minutes (720p on fully Unlimited (with external Unlimited(with external Unlimited (with external\\   charged Enduro battery) charged Enduro battery) power via USB) power via USB) power via USB How to achieve low latency feed streaming onto displays? The stream has a minimum latency of about 210ms. If you are seeing latency higher than that, we often find that as a result of using off-the-shelf libraries like ffmpeg which adds its own buffering. For preview and framing use cases, we don’t recommend using the live streaming RTMP protocol because it adds unnecessary steps in the pipeline, and puts the camera in the streaming preset, which offers little other control. A low latency streaming demo is available in the demos. How do I minimize latency of video preview with FFPLAY? FFPLAY by default will buffer remote streams. You can minimize the buffer using: --no-cache (receiving side) `-fflags nobuffer” (sender). However, the best latency can be achieved by building your own pipeline or ffmpegs library for decoding the bytes. Users should be able to achieve about 200-300 ms latency on the desktop and possibly optimize lower than that. How to view the video stream in VLC? To view the stream in VLC, you need to open network stream udp://@0.0.0.0:8554. You will still see latency because VLC uses its own caching. Power What are the power requirements for GoPro if connected over USB? All cameras have minimum power requirements, as specified here. As long as the power is supplied, cameras will be fully functional with or without an internal battery. Removing the battery and running on USB power alone will improve thermal performance and runtime. If you are seeing issues with insufficient power and have a charger with correct specs, the problems likely stem from low quality cables or low-quality adapters that are not able to consistently provide advertised amperage. We have encountered USB-C cables manufactured with poor quality control that transfer enough power only when their connectors face one side up, but not the other. We recommend using only high-quality components that deliver the correct power output How to enable automatic power on and off in wired setups? Cameras cannot be switched on remotely over USB, or “woken up” remotely after they “go to sleep”. The best workaround for this is via the GoPro Labs firmware that forces the camera to automatically switch on as soon as it detects USB power and switch off when the powering stops. Refer to the WAKE command here. Metadata Can I use the GPS track from the camera in real time? No. The GPS track on the camera as well as other metadata is not available until the file is written and saved. If the objective is to add metadata to the stream, currently the only option is to pull GPS data from another device (phone, wearable,… ) and sync it to the video feed. What can be accessed in the metadata file? Metadata exists as a proprietary GPMF (GoPro Metadata Format) and can be extracted from the file via API commands separately for GPS, Telemetry data, or the entire metadata container. The following data points can be extracted: Camera settings (Exposure time, ISO, Sensor Gain, White balance) Date and Time IMU: GPS, gyroscope, and accelerometer Smile detection Audio levels Face detection in bounding boxes Scene Classifiers (water, urban, vegetation, snow, beach, indoor) Is there a way to change the file names or otherwise classify my video file? Currently there are two options to do that, and both require GoPro Labs firmware. The stock firmware doesn’t provide that option. With GoPro Labs installed, you can either inject metadata into the file (and extract it later with the GPMF parser) or use custom naming for the file. Is there a way to add time stamps to the video files and mark specific moments? Open GoPro users can add time stamped markers, called “Hilights”, to flag specific moments in the video. Hilights can be injected into the video in the real time and then extracted for analytics or other post-processing purposes. The same Hilights are used in GoPro’s auto-editing engine Quik to determine the most interesting moments in the video. General Which cameras are supported by Open GoPro? The answer at a high level is >= Hero 9. However, there are also certain firmware requirements. For a complete answer, see the Specification. How to get the remaining timelapse capability? First check the value of Setting 128. Then depending on whether this is Photo or Video, use: Status 34 (Remaining photos) Status 35 (Remaining videos) Camera Logic Do commands operate as priority-wise or time-related? The cameras use first-in, first-out logic. Is there an option to send the commands in cyclic format instead of sending requests for each command? If you want to receive information asynchronously, it is possible via registering for BLE notifications. See an example (tracking battery) in the Python SDK. Troubleshooting If you are able to consistently reproduce a problem, please file a bug on Github Issues Why is the camera not advertising? If you have not yet paired to the camera with the desired device, then you need to first set the camera into pairing mode (Connections->Connect Device->Quick App). If you have already paired, then the camera should be advertising and ready to connect. If it is not advertising, it is possible you are already connected to it from a previous session. To be sure, power cycle both the camera and the peer device. Workaround for intermittent Wifi AP Connection failure On >= Hero 11, try disabling and then re-enabling the camera’s Wifi AP using the AP Control BLE Command Known Issues Relevant to All Supported Cameras Webcam does not enter idle mode once plugged in The webcam status will be wrongly reported as IDLE instead of OFF after a new USB connection. The best workaround for this is to call Webcam Start followed by Webcam Stop after connecting USB in order to make the webcam truly IDLE and thus willing to accept setting changes. Intermittent failure to connect to the cameras Wifi Access Point On rare occasions, connections to the camera’s Wifi AP will continuously fail until the camera is reset. It is possible to workaround this as described in Troubleshooting Spurious Protobuf Notifications sent once camera is connected in Station mode Once the camera has been connected in station mode (STA), it will start sending protobuf notifications with action ID 0xFF. These should be ignored. Hero 11 (v01.10.00) Specific Wired Communication is broken after update mode This is fixed by Resetting Connections and then re-pairing. Hero 13 (v01.10.00) Specific Webcam endpoints are broken. The following endpoints will always return 500 error status: Start Webcam Exit Webcam Preview Webcam", + "excerpt": "Frequently Asked Questions (FAQ) If you have somehow stumbled here first, note that there are specifications, demos, and tutorials which expand upon much of the information here. These can be found, among other places, from the home page. Connectivity What is the distance from the camera that BLE will still work? It is standard Bluetooth 4.0 range and it depends on external factors such as: Interference: anything interfering with the signal will shorten the range. The type of device that the camera is connected to: BT classification distinguishes 3 device classes based on their power levels. Depending on the class of the connected device, the range varies from less than 10 meters to 100 meters. Can I connect using WiFi only? Theoretically yes, if you already know the SSID, password, and the camera’s WiFi AP has been enabled. However, practically no because BLE is required in order to discover this information and configure the AP. Can I connect using BLE only? Yes, however there is some functionality that is not possible over BLE such as accessing the media list and downloading files. How to allow third-party devices to automatically discover close-by GoPro cameras? Devices can only be discovered via BLE by scanning for advertising GoPro cameras Multi Camera Setups How many devices can connect to the camera? Simultaneously, only one device can connect at a time. However, the camera stores BLE security keys and other connection information so it is possible to connect multiple devices sequentially. Is there currently a way to connect multiple cameras on the same Wifi network? No. Cameras can only be connected through Wi-Fi by becoming an access point itself (generating its own Wi-Fi network), not as a peripheral. What is the time offset between multiple cameras executing the same command? In cases when camera sync is important, we recommend using the USB connection, which minimizes the variance among devices. The time drift among cameras connected by USB cable to the same host will be up to ~35ms. Using BLE for that purpose will further increase it. Is there a way to precisely time sync cameras so the footage can be aligned during post-processing? The cameras set their time via GPS. By default, the camera seeks GPS at the beginning of every session, but this can be hindered by typical limitations of GPS signals. Additionally, there are two advanced options that require GoPro Labs firmware installed on the camera. The best bet is multi-cam GPS sync. Another option is precise time calibration via a dynamic QR scan from a smartphone or PC. Streaming What are the differences between the streaming options for GoPros? There are currently 3 different options on how to stream out of GoPro cameras. They are available either via Wi-Fi, USB, or both.   : Wifi :   : USB :       ViewFinder Preview LiveStream ViewFinder Preview Webcam Preview Webcam Orientation Floating or Fixed Landscape facing up Floating or Fixed Landscape: Floating or Fixed Landscape: Floating or Fixed Streaming Protocols UDP (MPEG-TS) RTMP UDP (MPEG-TS) UDP (MPEG-TS) UDP (MPEG-TS) \\         RTSP RTSP Connection Protocol Wifi - AP Mode WiFi - STA Mode NCM NCM NCM Resolution 480p, 720p 480p, 720p, 1080p 480p, 720p 720p, 1080p 720p, 1080p Frame Rate 30 30 30 30 30 Bitrate 2.5 - 4 mbps 0.8 - 8 mbps 2.5 - 4 mbps 6 mbps 6 mbps \\   depending on model configurable depending on model     Stabilization Basic Stabilization HyperSmooth or none Basic Stabilization None None Image Quality Basic Same as recorded content Basic Basic Same as recorded content Minimum Latency 210 ms > 100ms (un-stabilized) 210 ms 210 ms 210 ms \\     > 1,100ms (stabilized)       Audio None Stereo None None None Max Stream Time 150 minutes (720p on fully 85 minutes (720p on fully Unlimited (with external Unlimited(with external Unlimited (with external\\   charged Enduro battery) charged Enduro battery) power via USB) power via USB) power via USB How to achieve low latency feed streaming onto displays? The stream has a minimum latency of about 210ms. If you are seeing latency higher than that, we often find that as a result of using off-the-shelf libraries like ffmpeg which adds its own buffering. For preview and framing use cases, we don’t recommend using the live streaming RTMP protocol because it adds unnecessary steps in the pipeline, and puts the camera in the streaming preset, which offers little other control. A low latency streaming demo is available in the demos. How do I minimize latency of video preview with FFPLAY? FFPLAY by default will buffer remote streams. You can minimize the buffer using: --no-cache (receiving side) `-fflags nobuffer” (sender). However, the best latency can be achieved by building your own pipeline or ffmpegs library for decoding the bytes. Users should be able to achieve about 200-300 ms latency on the desktop and possibly optimize lower than that. How to view the video stream in VLC? To view the stream in VLC, you need to open network stream udp://@0.0.0.0:8554. You will still see latency because VLC uses its own caching. Power What are the power requirements for GoPro if connected over USB? All cameras have minimum power requirements, as specified here. As long as the power is supplied, cameras will be fully functional with or without an internal battery. Removing the battery and running on USB power alone will improve thermal performance and runtime. If you are seeing issues with insufficient power and have a charger with correct specs, the problems likely stem from low quality cables or low-quality adapters that are not able to consistently provide advertised amperage. We have encountered USB-C cables manufactured with poor quality control that transfer enough power only when their connectors face one side up, but not the other. We recommend using only high-quality components that deliver the correct power output How to enable automatic power on and off in wired setups? Cameras cannot be switched on remotely over USB, or “woken up” remotely after they “go to sleep”. The best workaround for this is via the GoPro Labs firmware that forces the camera to automatically switch on as soon as it detects USB power and switch off when the powering stops. Refer to the WAKE command here. Metadata Can I use the GPS track from the camera in real time? No. The GPS track on the camera as well as other metadata is not available until the file is written and saved. If the objective is to add metadata to the stream, currently the only option is to pull GPS data from another device (phone, wearable,… ) and sync it to the video feed. What can be accessed in the metadata file? Metadata exists as a proprietary GPMF (GoPro Metadata Format) and can be extracted from the file via API commands separately for GPS, Telemetry data, or the entire metadata container. The following data points can be extracted: Camera settings (Exposure time, ISO, Sensor Gain, White balance) Date and Time IMU: GPS, gyroscope, and accelerometer Smile detection Audio levels Face detection in bounding boxes Scene Classifiers (water, urban, vegetation, snow, beach, indoor) Is there a way to change the file names or otherwise classify my video file? Currently there are two options to do that, and both require GoPro Labs firmware. The stock firmware doesn’t provide that option. With GoPro Labs installed, you can either inject metadata into the file (and extract it later with the GPMF parser) or use custom naming for the file. Is there a way to add time stamps to the video files and mark specific moments? Open GoPro users can add time stamped markers, called “Hilights”, to flag specific moments in the video. Hilights can be injected into the video in the real time and then extracted for analytics or other post-processing purposes. The same Hilights are used in GoPro’s auto-editing engine Quik to determine the most interesting moments in the video. General Which cameras are supported by Open GoPro? The answer at a high level is >= Hero 9. However, there are also certain firmware requirements. For a complete answer, see the Specification. How to get the remaining timelapse capability? First check the value of Setting 128. Then depending on whether this is Photo or Video, use: Status 34 (Remaining photos) Status 35 (Remaining videos) Camera Logic Do commands operate as priority-wise or time-related? The cameras use first-in, first-out logic. Is there an option to send the commands in cyclic format instead of sending requests for each command? If you want to receive information asynchronously, it is possible via registering for BLE notifications. See an example (tracking battery) in the Python SDK. Troubleshooting If you are able to consistently reproduce a problem, please file a bug on Github Issues Why is the camera not advertising? If you have not yet paired to the camera with the desired device, then you need to first set the camera into pairing mode (Connections->Connect Device->Quick App). If you have already paired, then the camera should be advertising and ready to connect. If it is not advertising, it is possible you are already connected to it from a previous session. To be sure, power cycle both the camera and the peer device. Workaround for intermittent Wifi AP Connection failure On >= Hero 11, try disabling and then re-enabling the camera’s Wifi AP using the AP Control BLE Command Known Issues Relevant to All Supported Cameras Webcam does not enter idle mode once plugged in The webcam status will be wrongly reported as IDLE instead of OFF after a new USB connection. The best workaround for this is to call Webcam Start followed by Webcam Stop after connecting USB in order to make the webcam truly IDLE and thus willing to accept setting changes. Intermittent failure to connect to the cameras Wifi Access Point On rare occasions, connections to the camera’s Wifi AP will continuously fail until the camera is reset. It is possible to workaround this as described in Troubleshooting Spurious Protobuf Notifications sent once camera is connected in Station mode Once the camera has been connected in station mode (STA), it will start sending protobuf notifications with action ID 0xFF. These should be ignored. Hero 11 (v01.10.00) Specific Wired Communication is broken after update mode This is fixed by Resetting Connections and then re-pairing. Hero 13 (v01.10.00) Specific Webcam endpoints are broken. The following endpoints will always return 500 error status: Start Webcam Exit Webcam Preview Webcam Camera is not discoverable via MDNS. The camera does not advertise the _gopro-web service.", "categories": [], "tags": [], "url": "/OpenGoPro/faq#" @@ -36,14 +36,14 @@ var jekyllStore = [ }, { "title": "Tutorial 3: Parse BLE TLV Responses: ", - "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to parse BLE Type-Length-Value (TLV) Responses. Besides TLV, some BLE operations instead return protobuf responses. These are not considered here and will be discussed in a future tutorial 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 are present and configured correctly. It is suggested that you have first completed the connect and sending commands tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 3 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Parsing a One Packet TLV Response You can test parsing a one packet TLV response with your camera through BLE using the following script: $ python ble_command_get_version.py See the help for parameter definitions: $ python ble_command_get_version.py --help usage: ble_command_get_version.py [-h] [-i IDENTIFIER] Connect to a GoPro camera via BLE, then get the Open GoPro version. 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 Parsing Multiple Packet TLV Responses You can test parsing multiple packet TVL responses with your camera through BLE using the following script: $ python ble_command_get_hardware_info.py See the help for parameter definitions: $ 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 hardware info. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 3” 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. Perform Tutorial 3 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. Setup We must first connect as was discussed in the connecting BLE tutorial. 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 performing specific operations where this works and we know that the sequence always appears as such (connection sequence left out for brevity): GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start)) In actuality, responses can be more complicated. As described in the BLE Spec, responses can be be comprised of multiple packets where each packet is <= 20 bytes such as: GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation)) This requires the implementation of accumulating and parsing algorithms which will be described below. 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) Operation ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes 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 Set Shutter. It returned a response of: 02:01:00 This equates to: 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. Responses with Payload 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: Response With Payload In this tutorial, we will walk through creating a simple parser to parse the Open GoPro Get Version Command which is an example of such an operation. 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. First, we send the Get Version Command to the Command Request UUID in the same manner as commands were sent in the previous tutorial: python kotlin 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 receive a response at the expected handle (as a TLV Response). This is logged as: 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 val versionRequest = ubyteArrayOf(0x01U, 0x51U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, versionRequest) var tlvResponse = receivedResponses.receive() as Response.Tlv We then receive a response at the expected handle. This is logged as: This is logged as such: 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 CQ_COMMAND_RSP Received packet of length 6. 0 bytes remaining This response equates to: 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 response payload contains 4 additional bytes that need to be parsed. Using the information from the Get Version Documentation, we know to parse this as: 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 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. python kotlin The snippets of code included in this section are taken from the notification handler 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] The snippets of code included in this section are taken from the Response.Tlv.Parse method // Parse header bytes tlvResponse.parse() ... open fun parse() { require(isReceived) id = rawBytes[0].toInt() status = rawBytes[1].toInt() // Store remainder as payload payload = rawBytes.drop(2).toUByteArray() } 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: python kotlin 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: Received a response to command_id=81 with status=0, payload=01:02:01:00 The version is Open GoPro 2.0 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. 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: Received response: ID: 81, Status: 0, Payload: 01:02:01:00 Got the Open GoPro version successfully: 2.0 Quiz time! 📚 ✏️ What is the maximum size of an individual notification response packet at the Open GoPro application layer? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. Responses can be composed of multiple packets where each packet is at maximum 20 bytes. What is the maximum amount of bytes that one response can be composed of? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There is no limit on the amount of packets that can comprise a response. How many packets are command responses composed of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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. How many packets are setting responses comprised of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. 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. 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 headers as they are defined, reproduced for reference here: Byte 1 Byte 2 (optional) Byte 3 (optional) 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 0: Start 00: General Message Length: 5 bits 0: Start 01: Extended (13-bit) Message Length: 13 bits 0: Start 10: Extended (16-bit) Message Length: 16 bits 0: Start 11: Reserved 1: Continuation The basic accumulation algorithm (which is implemented in the Response.Accumulate method) is as follows: Is the continuation bit set? python kotlin The example script that will be walked through for this section is ble_command_get_hardware_info.py. if buf[0] & CONT_MASK: buf.pop(0) else: ... 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 ... No, the continuation bit was not set. Therefore create new response, then get its length. python kotlin 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:] // This is a new packet so start with empty array packet = 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\") } } Append current packet to response and decrement bytes remaining. python kotlin Append payload to buffer and update remaining / complete self.bytes.extend(buf) self.bytes_remaining -= len(buf) // Accumulate the payload now that headers are handled and dropped packet += buf bytesRemaining -= buf.size In the notification handler, we are then enqueueing the received response if there are no bytes remaining. python kotlin if response.is_received: ... 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: 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() if (response.isReceived) { if (uuid == GoProUUID.CQ_COMMAND_RSP) { CoroutineScope(Dispatchers.IO).launch { receivedResponses.send(response) } } ... NoYesDecrement bytes remainingYesNoRead Available PacketContinuation bit set?Create new empty responseGet bytes remaining, i.e. lengthAppend packet to accumulating responseBytes remaining == 0?Parse Received Packet We can see this in action when we send the Get Hardware Info Command: python kotlin 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() val hardwareInfoRequest = ubyteArrayOf(0x01U, 0x3CU) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, hardwareInfoRequest) 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. This notification handler is only designed to handle TlvResponses. This is fine for this tutorial since that is all we will be receiving. python kotlin 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() 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) 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) private fun notificationHandler(characteristic: UUID, data: UByteArray) { ... 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) } } } We can see the individual packets being accumulated in the log: python kotlin 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 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 At this point the response has been accumulated. We then parse and log the payload using the Get Hardware Info response documentation: python kotlin hardware_info = HardwareInfo.from_bytes(response.payload) logger.info(f\"Received hardware info: {hardware_info}\") where the parsing is done as such: @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: 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\" } tlvResponse.parse() val hardwareInfo = HardwareInfo.fromBytes(tlvResponse.payload) where the parsing is done as such: 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) } This logs as: Got the Hardware Info successfully: HardwareInfo( modelNumber=1040187392, modelName=HERO12 Black, firmwareVersion=H23.01.01.99.56, serialNumber=C3501324500702, apSsid=GP24500702, apMacAddress=2674f7f66104 ) Quiz time! 📚 ✏️ How can we know that a response has been completely received? A: The stop bit will be set in the header B: The response has accumulated length bytes C: By checking for the end of frame (EOF) sentinel character Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. 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 See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 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. To learn about a different type of operation (Queries), go to the next tutorial.", + "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to parse BLE Type-Length-Value (TLV) Responses. Besides TLV, some BLE operations instead return protobuf responses. These are not considered here and will be discussed in a future tutorial 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 are present and configured correctly. It is suggested that you have first completed the connect and sending commands tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 3 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Parsing a One Packet TLV Response You can test parsing a one packet TLV response with your camera through BLE using the following script: $ python ble_command_get_version.py See the help for parameter definitions: $ python ble_command_get_version.py --help usage: ble_command_get_version.py [-h] [-i IDENTIFIER] Connect to a GoPro camera via BLE, then get the Open GoPro version. 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 Parsing Multiple Packet TLV Responses You can test parsing multiple packet TVL responses with your camera through BLE using the following script: $ python ble_command_get_hardware_info.py See the help for parameter definitions: $ 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 hardware info. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 3” 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. Perform Tutorial 3 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. Setup We must first connect as was discussed in the connecting BLE tutorial. 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 performing specific operations where this works and we know that the sequence always appears as such (connection sequence left out for brevity): GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start)) In actuality, responses can be more complicated. As described in the BLE Spec, responses can be be comprised of multiple packets where each packet is <= 20 bytes such as: GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation)) This requires the implementation of accumulating and parsing algorithms which will be described below. 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) Operation ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes 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 Set Shutter. It returned a response of: 02:01:00 This equates to: 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. Responses with Payload 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: Response With Payload In this tutorial, we will walk through creating a simple parser to parse the Open GoPro Get Version Command which is an example of such an operation. 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. First, we send the Get Version Command to the Command Request UUID in the same manner as commands were sent in the previous tutorial: python kotlin 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 receive a response at the expected handle (as a TLV Response). This is logged as: 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 val versionRequest = ubyteArrayOf(0x01U, 0x51U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, versionRequest) var tlvResponse = receivedResponses.receive() as Response.Tlv We then receive a response at the expected handle. This is logged as: This is logged as such: 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 CQ_COMMAND_RSP Received packet of length 6. 0 bytes remaining This response equates to: 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 response payload contains 4 additional bytes that need to be parsed. Using the information from the Get Version Documentation, we know to parse this as: 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 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. python kotlin The snippets of code included in this section are taken from the notification handler 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] The snippets of code included in this section are taken from the Response.Tlv.Parse method // Parse header bytes tlvResponse.parse() ... open fun parse() { require(isReceived) id = rawBytes[0].toInt() status = rawBytes[1].toInt() // Store remainder as payload payload = rawBytes.drop(2).toUByteArray() } 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: python kotlin 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: Received a response to command_id=81 with status=0, payload=01:02:01:00 The version is Open GoPro 2.0 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. 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: Received response: ID: 81, Status: 0, Payload: 01:02:01:00 Got the Open GoPro version successfully: 2.0 Quiz time! 📚 ✏️ What is the maximum size of an individual notification response packet at the Open GoPro application layer? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. Responses can be composed of multiple packets where each packet is at maximum 20 bytes. What is the maximum amount of bytes that one response can be composed of? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There is no limit on the amount of packets that can comprise a response. How many packets are command responses composed of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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. How many packets are setting responses comprised of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. 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. 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 headers as they are defined, reproduced for reference here: Byte 1 Byte 2 (optional) Byte 3 (optional) 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 0: Start 00: General Message Length: 5 bits 0: Start 01: Extended (13-bit) Message Length: 13 bits 0: Start 10: Extended (16-bit) Message Length: 16 bits 0: Start 11: Reserved 1: Continuation The basic accumulation algorithm (which is implemented in the Response.Accumulate method) is as follows: Is the continuation bit set? python kotlin The example script that will be walked through for this section is ble_command_get_hardware_info.py. if buf[0] & CONT_MASK: buf.pop(0) else: ... 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 ... No, the continuation bit was not set. Therefore create new response, then get its length. python kotlin 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:] // This is a new packet so start with empty array packet = 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\") } } Append current packet to response and decrement bytes remaining. python kotlin Append payload to buffer and update remaining / complete self.bytes.extend(buf) self.bytes_remaining -= len(buf) // Accumulate the payload now that headers are handled and dropped packet += buf bytesRemaining -= buf.size In the notification handler, we are then enqueueing the received response if there are no bytes remaining. python kotlin if response.is_received: ... 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: 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() if (response.isReceived) { if (uuid == GoProUUID.CQ_COMMAND_RSP) { CoroutineScope(Dispatchers.IO).launch { receivedResponses.send(response) } } ... No Yes Decrement bytes remaining Yes No Read Available Packet Continuation bit set? Create new empty response Get bytes remaining, i.e. length Append packet to accumulating response Bytes remaining == 0? Parse Received Packet We can see this in action when we send the Get Hardware Info Command: python kotlin 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() val hardwareInfoRequest = ubyteArrayOf(0x01U, 0x3CU) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, hardwareInfoRequest) 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. This notification handler is only designed to handle TlvResponses. This is fine for this tutorial since that is all we will be receiving. python kotlin 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() 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) 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) private fun notificationHandler(characteristic: UUID, data: UByteArray) { ... 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) } } } We can see the individual packets being accumulated in the log: python kotlin 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 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 At this point the response has been accumulated. We then parse and log the payload using the Get Hardware Info response documentation: python kotlin hardware_info = HardwareInfo.from_bytes(response.payload) logger.info(f\"Received hardware info: {hardware_info}\") where the parsing is done as such: @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: 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\" } tlvResponse.parse() val hardwareInfo = HardwareInfo.fromBytes(tlvResponse.payload) where the parsing is done as such: 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) } This logs as: Got the Hardware Info successfully: HardwareInfo( modelNumber=1040187392, modelName=HERO12 Black, firmwareVersion=H23.01.01.99.56, serialNumber=C3501324500702, apSsid=GP24500702, apMacAddress=2674f7f66104 ) Quiz time! 📚 ✏️ How can we know that a response has been completely received? A: The stop bit will be set in the header B: The response has accumulated length bytes C: By checking for the end of frame (EOF) sentinel character Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. 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 See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 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. To learn about a different type of operation (Queries), go to the next tutorial.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/parse-ble-responses#" }, { "title": "Tutorial 4: BLE TLV Queries: ", - "excerpt": "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 operations that are initiated by writing to the Query UUID and receiving responses via the Query Response UUID. A list of queries can be found in the Query ID Table. It is important to distinguish between queries and commands because they each have different request and response packet formats. 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. Requirements It is assumed that the hardware and software requirements from the connecting BLE tutorial are present and configured correctly. It is suggested that you have first completed the connect, sending commands, and parsing responses tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 4 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Individual Query Poll You can test an individual query poll with your camera through BLE using the following script: $ python ble_query_poll_resolution_value.py See the help for parameter definitions: $ 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. 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 Multiple Simultaneous Query Polls You can test querying multiple queries simultaneously with your camera through BLE using the following script: $ python ble_query_poll_multiple_setting_values.py See the help for parameter definitions: $ 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. 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 Registering for Query Push Notifications You can test registering for querties and receiving push notifications with your camera through BLE using the following script: $ python ble_query_register_resolution_value_updates.py See the help for parameter definitions: $ 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. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 4” 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. Perform Tutorial 4 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. Setup We must first connect as was discussed in the connecting BLE tutorial. python kotlin 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. 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: 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) The code above is taken from ble_query_poll_resolution_value.py We are defining a resolution enum that will be updated as we receive new resolutions: private enum class Resolution(val value: UByte) { RES_4K(1U), RES_2_7K(4U), RES_2_7K_4_3(6U), RES_1080(9U), RES_4K_4_3(18U), RES_5K(24U); companion object { private val valueMap: Map<UByte, Resolution> by lazy { values().associateBy { it.value } } fun fromValue(value: UByte) = valueMap.getValue(value) } } private lateinit var resolution: Resolution There are two methods to query status / setting information, each of which will be described in a following section: Polling Query Information 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 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. python kotlin 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:] 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) } yesnoParse Query IDParse StatusMore data?Get Value IDGet Value LengthGet Valuedone How many packets are query responses? A: Always 1 packet B: Always multiple packets C: Can be 1 or multiple packets Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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). Which field is not common to all TLV responses? A: length B: status C: ID D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. 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 queries: Query ID Request Query 0x12 [Get Setting value(s)](/OpenGoPro/ble/features/query.htmlget-setting-values) len:12:xx:xx 0x13 [Get Status value(s)](/OpenGoPro/ble/features/query.htmlget-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. Since they are two separate queries, combination of settings / statuses can not be polled simultaneously. Here is a generic sequence diagram (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)Get Setting value(s) queries written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUID The number of notification responses will vary depending on the amount of settings that have been queried. 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 Individual Query Poll Here we will walk through an example of polling one setting (Resolution). First we send the query: python kotlin The sample code can be found in in ble_query_poll_resolution_value.py. 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) val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) 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. python kotlin Wait to receive the notification response response = await received_responses.get() response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) which logs as such: 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 // Wait to receive the response and then convert it to resolution val queryResponse = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) which logs as such: 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:09 Received response on CQ_QUERY_RSP Received packet of length 5. 0 bytes remaining Received Query Response Camera resolution is RES_1080 For verification purposes, we are then changing the resolution and polling again to verify that the setting has changed: python kotlin 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: 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... 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: 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 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: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_2_7K 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 that the query now includes 3 settings: Resolution, FPS, and FOV. python kotlin RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 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 TODO The length (first byte of the query) has been increased to 4 to accommodate the extra settings We are also parsing the response to get all 3 values: python kotlin 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])}\") TODO When we are storing the updated setting, we are just taking the first byte (i..e index 0). A real-world implementation would need to know the length (and type) of the setting / status response by the ID. For example, sometimes settings / statuses are bytes, words, strings, etc. They are then printed to the log which will look like the following: python kotlin 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 TODO In general, we can parse query values by looking at relevant documentation linked from the Setting or Status ID tables. For example (for settings): ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 Query All 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 Quiz time! 📚 ✏️ How can we poll the encoding status and the resolution setting using one query? A: Concatenate a &8216;Get Setting Value&8217; query and a &8216;Get Status&8217; query with the relevant ID&8217;s B: Concatenate the &8216;Get All Setting&8217; and &8216;Get All Status&8217; queries. C: It is not possible Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. It is not possible to concatenate queries. This would result in an unknown sequence of bytes from the camera&8217;s perspective. So it is not possible to get a setting value and a status value in one 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. Registering for Query Push Notifications 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 queries are: Query ID Request Query 0x52 [Register updates for setting(s)](/OpenGoPro/ble/features/query.htmlregister-for-setting-value-updates) len:52:xx:xx 0x53 [Register updates for status(es)](/OpenGoPro/ble/features/query.htmlregister-for-status-value-updates) len:53:xx:xx 0x72 [Unregister updates for setting(s)](/OpenGoPro/ble/features/query.htmlunregister-for-setting-value-updates) len:72:xx:xx 0x73 [Unregister updates for status(es)](/OpenGoPro/ble/features/query.htmlunregister-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: Query ID Response 0x92 Setting Value Push Notification 0x93 Status Value Push Notification Here is a generic sequence diagram of how this looks (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)loop[Setting changes]loop[Settingchanges]Register updates for settingNotification Response and Current Setting ValueSetting changesPush notification of new setting valueUnregister updates for settingNotification ResponseSetting changes That is, after registering for push notifications for a given query, notification responses will continuously be sent whenever the query changes until the client unregisters for push notifications for the given query. The initial response to the Register query also contains the current setting / status value. We will walk through an example of this below: First, let’s register for updates when the resolution setting changes: python kotlin 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() val registerResolutionUpdates = ubyteArrayOf(0x02U, 0x52U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, registerResolutionUpdates) 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. python kotlin response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) logger.info(f\"Resolution is currently {resolution}\") This will show in the log as such: 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 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: 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 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 and waiting to receive the update. notification.. python kotlin 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: 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... 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 = (receivedResponses.receive() as Response.Tlv).apply { parse() } // Verify we receive the update from the camera when the resolution changes while (resolution != targetResolution) { val queryNotification = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) } We can see change happen in the log: 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 Resolution successfully changed Waiting for camera to inform us about the resolution change Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:92:00:02:01:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:92:00:02:01:04 Received resolution query response Resolution is now RES_2_7K In this case, the Query ID is 0x92 (Setting Value Push Notification) as expected. Multiple push notifications can be registered / received in a similar manner that multiple queries were polled above Quiz time! 📚 ✏️ True or False: We can still poll a given query value while we are currently registered to receive push notifications for it. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. While there is probably not a good reason to do so, there is nothing preventing polling in this manner. True or False: A push notification for a registered setting will only ever contain query information about one setting ID. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. It is possible for push notifications to contain multiple setting ID&8217;s if both setting ID&8217;s have push notifications registered and both settings change at the same time. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now query any of the settings / statuses from the camera using one of the above patterns.", + "excerpt": "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 operations that are initiated by writing to the Query UUID and receiving responses via the Query Response UUID. A list of queries can be found in the Query ID Table. It is important to distinguish between queries and commands because they each have different request and response packet formats. 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. Requirements It is assumed that the hardware and software requirements from the connecting BLE tutorial are present and configured correctly. It is suggested that you have first completed the connect, sending commands, and parsing responses tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 4 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Individual Query Poll You can test an individual query poll with your camera through BLE using the following script: $ python ble_query_poll_resolution_value.py See the help for parameter definitions: $ 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. 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 Multiple Simultaneous Query Polls You can test querying multiple queries simultaneously with your camera through BLE using the following script: $ python ble_query_poll_multiple_setting_values.py See the help for parameter definitions: $ 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. 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 Registering for Query Push Notifications You can test registering for querties and receiving push notifications with your camera through BLE using the following script: $ python ble_query_register_resolution_value_updates.py See the help for parameter definitions: $ 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. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 4” 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. Perform Tutorial 4 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. Setup We must first connect as was discussed in the connecting BLE tutorial. python kotlin 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. 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: 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) The code above is taken from ble_query_poll_resolution_value.py We are defining a resolution enum that will be updated as we receive new resolutions: private enum class Resolution(val value: UByte) { RES_4K(1U), RES_2_7K(4U), RES_2_7K_4_3(6U), RES_1080(9U), RES_4K_4_3(18U), RES_5K(24U); companion object { private val valueMap: Map<UByte, Resolution> by lazy { values().associateBy { it.value } } fun fromValue(value: UByte) = valueMap.getValue(value) } } private lateinit var resolution: Resolution There are two methods to query status / setting information, each of which will be described in a following section: Polling Query Information 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 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. python kotlin 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:] 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) } yes no Parse Query ID Parse Status More data? Get Value ID Get Value Length Get Value done How many packets are query responses? A: Always 1 packet B: Always multiple packets C: Can be 1 or multiple packets Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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). Which field is not common to all TLV responses? A: length B: status C: ID D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. 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 queries: Query ID Request Query 0x12 [Get Setting value(s)](/OpenGoPro/ble/features/query.htmlget-setting-values) len:12:xx:xx 0x13 [Get Status value(s)](/OpenGoPro/ble/features/query.htmlget-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. Since they are two separate queries, combination of settings / statuses can not be polled simultaneously. Here is a generic sequence diagram (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)Get Setting value(s) queries written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUID The number of notification responses will vary depending on the amount of settings that have been queried. 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 Individual Query Poll Here we will walk through an example of polling one setting (Resolution). First we send the query: python kotlin The sample code can be found in in ble_query_poll_resolution_value.py. 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) val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) 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. python kotlin Wait to receive the notification response response = await received_responses.get() response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) which logs as such: 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 // Wait to receive the response and then convert it to resolution val queryResponse = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) which logs as such: 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:09 Received response on CQ_QUERY_RSP Received packet of length 5. 0 bytes remaining Received Query Response Camera resolution is RES_1080 For verification purposes, we are then changing the resolution and polling again to verify that the setting has changed: python kotlin 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: 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... 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: 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 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: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_2_7K 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 that the query now includes 3 settings: Resolution, FPS, and FOV. python kotlin RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 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 TODO The length (first byte of the query) has been increased to 4 to accommodate the extra settings We are also parsing the response to get all 3 values: python kotlin 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])}\") TODO When we are storing the updated setting, we are just taking the first byte (i..e index 0). A real-world implementation would need to know the length (and type) of the setting / status response by the ID. For example, sometimes settings / statuses are bytes, words, strings, etc. They are then printed to the log which will look like the following: python kotlin 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 TODO In general, we can parse query values by looking at relevant documentation linked from the Setting or Status ID tables. For example (for settings): ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 Query All 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 Quiz time! 📚 ✏️ How can we poll the encoding status and the resolution setting using one query? A: Concatenate a &8216;Get Setting Value&8217; query and a &8216;Get Status&8217; query with the relevant ID&8217;s B: Concatenate the &8216;Get All Setting&8217; and &8216;Get All Status&8217; queries. C: It is not possible Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. It is not possible to concatenate queries. This would result in an unknown sequence of bytes from the camera&8217;s perspective. So it is not possible to get a setting value and a status value in one 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. Registering for Query Push Notifications 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 queries are: Query ID Request Query 0x52 [Register updates for setting(s)](/OpenGoPro/ble/features/query.htmlregister-for-setting-value-updates) len:52:xx:xx 0x53 [Register updates for status(es)](/OpenGoPro/ble/features/query.htmlregister-for-status-value-updates) len:53:xx:xx 0x72 [Unregister updates for setting(s)](/OpenGoPro/ble/features/query.htmlunregister-for-setting-value-updates) len:72:xx:xx 0x73 [Unregister updates for status(es)](/OpenGoPro/ble/features/query.htmlunregister-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: Query ID Response 0x92 Setting Value Push Notification 0x93 Status Value Push Notification Here is a generic sequence diagram of how this looks (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)loop[Setting changes]loop[Settingchanges]Register updates for settingNotification Response and Current Setting ValueSetting changesPush notification of new setting valueUnregister updates for settingNotification ResponseSetting changes That is, after registering for push notifications for a given query, notification responses will continuously be sent whenever the query changes until the client unregisters for push notifications for the given query. The initial response to the Register query also contains the current setting / status value. We will walk through an example of this below: First, let’s register for updates when the resolution setting changes: python kotlin 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() val registerResolutionUpdates = ubyteArrayOf(0x02U, 0x52U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, registerResolutionUpdates) 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. python kotlin response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) logger.info(f\"Resolution is currently {resolution}\") This will show in the log as such: 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 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: 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 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 and waiting to receive the update. notification.. python kotlin 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: 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... 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 = (receivedResponses.receive() as Response.Tlv).apply { parse() } // Verify we receive the update from the camera when the resolution changes while (resolution != targetResolution) { val queryNotification = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) } We can see change happen in the log: 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 Resolution successfully changed Waiting for camera to inform us about the resolution change Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:92:00:02:01:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:92:00:02:01:04 Received resolution query response Resolution is now RES_2_7K In this case, the Query ID is 0x92 (Setting Value Push Notification) as expected. Multiple push notifications can be registered / received in a similar manner that multiple queries were polled above Quiz time! 📚 ✏️ True or False: We can still poll a given query value while we are currently registered to receive push notifications for it. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. While there is probably not a good reason to do so, there is nothing preventing polling in this manner. True or False: A push notification for a registered setting will only ever contain query information about one setting ID. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. It is possible for push notifications to contain multiple setting ID&8217;s if both setting ID&8217;s have push notifications registered and both settings change at the same time. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now query any of the settings / statuses from the camera using one of the above patterns.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/ble-queries#" diff --git a/assets/js/lunr/lunr-store.js b/assets/js/lunr/lunr-store.js index 91e89bd0..4ec0f48e 100644 --- a/assets/js/lunr/lunr-store.js +++ b/assets/js/lunr/lunr-store.js @@ -1,7 +1,7 @@ var jekyllStore = [ { "title": "FAQ and Known Issues: ", - "excerpt": "Frequently Asked Questions (FAQ) If you have somehow stumbled here first, note that there are specifications, demos, and tutorials which expand upon much of the information here. These can be found, among other places, from the home page. Connectivity What is the distance from the camera that BLE will still work? It is standard Bluetooth 4.0 range and it depends on external factors such as: Interference: anything interfering with the signal will shorten the range. The type of device that the camera is connected to: BT classification distinguishes 3 device classes based on their power levels. Depending on the class of the connected device, the range varies from less than 10 meters to 100 meters. Can I connect using WiFi only? Theoretically yes, if you already know the SSID, password, and the camera’s WiFi AP has been enabled. However, practically no because BLE is required in order to discover this information and configure the AP. Can I connect using BLE only? Yes, however there is some functionality that is not possible over BLE such as accessing the media list and downloading files. How to allow third-party devices to automatically discover close-by GoPro cameras? Devices can only be discovered via BLE by scanning for advertising GoPro cameras Multi Camera Setups How many devices can connect to the camera? Simultaneously, only one device can connect at a time. However, the camera stores BLE security keys and other connection information so it is possible to connect multiple devices sequentially. Is there currently a way to connect multiple cameras on the same Wifi network? No. Cameras can only be connected through Wi-Fi by becoming an access point itself (generating its own Wi-Fi network), not as a peripheral. What is the time offset between multiple cameras executing the same command? In cases when camera sync is important, we recommend using the USB connection, which minimizes the variance among devices. The time drift among cameras connected by USB cable to the same host will be up to ~35ms. Using BLE for that purpose will further increase it. Is there a way to precisely time sync cameras so the footage can be aligned during post-processing? The cameras set their time via GPS. By default, the camera seeks GPS at the beginning of every session, but this can be hindered by typical limitations of GPS signals. Additionally, there are two advanced options that require GoPro Labs firmware installed on the camera. The best bet is multi-cam GPS sync. Another option is precise time calibration via a dynamic QR scan from a smartphone or PC. Streaming What are the differences between the streaming options for GoPros? There are currently 3 different options on how to stream out of GoPro cameras. They are available either via Wi-Fi, USB, or both.   : Wifi :   : USB :       ViewFinder Preview LiveStream ViewFinder Preview Webcam Preview Webcam Orientation Floating or Fixed Landscape facing up Floating or Fixed Landscape: Floating or Fixed Landscape: Floating or Fixed Streaming Protocols UDP (MPEG-TS) RTMP UDP (MPEG-TS) UDP (MPEG-TS) UDP (MPEG-TS) \\         RTSP RTSP Connection Protocol Wifi - AP Mode WiFi - STA Mode NCM NCM NCM Resolution 480p, 720p 480p, 720p, 1080p 480p, 720p 720p, 1080p 720p, 1080p Frame Rate 30 30 30 30 30 Bitrate 2.5 - 4 mbps 0.8 - 8 mbps 2.5 - 4 mbps 6 mbps 6 mbps \\   depending on model configurable depending on model     Stabilization Basic Stabilization HyperSmooth or none Basic Stabilization None None Image Quality Basic Same as recorded content Basic Basic Same as recorded content Minimum Latency 210 ms > 100ms (un-stabilized) 210 ms 210 ms 210 ms \\     > 1,100ms (stabilized)       Audio None Stereo None None None Max Stream Time 150 minutes (720p on fully 85 minutes (720p on fully Unlimited (with external Unlimited(with external Unlimited (with external\\   charged Enduro battery) charged Enduro battery) power via USB) power via USB) power via USB How to achieve low latency feed streaming onto displays? The stream has a minimum latency of about 210ms. If you are seeing latency higher than that, we often find that as a result of using off-the-shelf libraries like ffmpeg which adds its own buffering. For preview and framing use cases, we don’t recommend using the live streaming RTMP protocol because it adds unnecessary steps in the pipeline, and puts the camera in the streaming preset, which offers little other control. A low latency streaming demo is available in the demos. How do I minimize latency of video preview with FFPLAY? FFPLAY by default will buffer remote streams. You can minimize the buffer using: --no-cache (receiving side) `-fflags nobuffer” (sender). However, the best latency can be achieved by building your own pipeline or ffmpegs library for decoding the bytes. Users should be able to achieve about 200-300 ms latency on the desktop and possibly optimize lower than that. How to view the video stream in VLC? To view the stream in VLC, you need to open network stream udp://@0.0.0.0:8554. You will still see latency because VLC uses its own caching. Power What are the power requirements for GoPro if connected over USB? All cameras have minimum power requirements, as specified here. As long as the power is supplied, cameras will be fully functional with or without an internal battery. Removing the battery and running on USB power alone will improve thermal performance and runtime. If you are seeing issues with insufficient power and have a charger with correct specs, the problems likely stem from low quality cables or low-quality adapters that are not able to consistently provide advertised amperage. We have encountered USB-C cables manufactured with poor quality control that transfer enough power only when their connectors face one side up, but not the other. We recommend using only high-quality components that deliver the correct power output How to enable automatic power on and off in wired setups? Cameras cannot be switched on remotely over USB, or “woken up” remotely after they “go to sleep”. The best workaround for this is via the GoPro Labs firmware that forces the camera to automatically switch on as soon as it detects USB power and switch off when the powering stops. Refer to the WAKE command here. Metadata Can I use the GPS track from the camera in real time? No. The GPS track on the camera as well as other metadata is not available until the file is written and saved. If the objective is to add metadata to the stream, currently the only option is to pull GPS data from another device (phone, wearable,… ) and sync it to the video feed. What can be accessed in the metadata file? Metadata exists as a proprietary GPMF (GoPro Metadata Format) and can be extracted from the file via API commands separately for GPS, Telemetry data, or the entire metadata container. The following data points can be extracted: Camera settings (Exposure time, ISO, Sensor Gain, White balance) Date and Time IMU: GPS, gyroscope, and accelerometer Smile detection Audio levels Face detection in bounding boxes Scene Classifiers (water, urban, vegetation, snow, beach, indoor) Is there a way to change the file names or otherwise classify my video file? Currently there are two options to do that, and both require GoPro Labs firmware. The stock firmware doesn’t provide that option. With GoPro Labs installed, you can either inject metadata into the file (and extract it later with the GPMF parser) or use custom naming for the file. Is there a way to add time stamps to the video files and mark specific moments? Open GoPro users can add time stamped markers, called “Hilights”, to flag specific moments in the video. Hilights can be injected into the video in the real time and then extracted for analytics or other post-processing purposes. The same Hilights are used in GoPro’s auto-editing engine Quik to determine the most interesting moments in the video. General Which cameras are supported by Open GoPro? The answer at a high level is >= Hero 9. However, there are also certain firmware requirements. For a complete answer, see the Specification. How to get the remaining timelapse capability? First check the value of Setting 128. Then depending on whether this is Photo or Video, use: Status 34 (Remaining photos) Status 35 (Remaining videos) Camera Logic Do commands operate as priority-wise or time-related? The cameras use first-in, first-out logic. Is there an option to send the commands in cyclic format instead of sending requests for each command? If you want to receive information asynchronously, it is possible via registering for BLE notifications. See an example (tracking battery) in the Python SDK. Troubleshooting If you are able to consistently reproduce a problem, please file a bug on Github Issues Why is the camera not advertising? If you have not yet paired to the camera with the desired device, then you need to first set the camera into pairing mode (Connections->Connect Device->Quick App). If you have already paired, then the camera should be advertising and ready to connect. If it is not advertising, it is possible you are already connected to it from a previous session. To be sure, power cycle both the camera and the peer device. Workaround for intermittent Wifi AP Connection failure On >= Hero 11, try disabling and then re-enabling the camera’s Wifi AP using the AP Control BLE Command Known Issues Relevant to All Supported Cameras Webcam does not enter idle mode once plugged in The webcam status will be wrongly reported as IDLE instead of OFF after a new USB connection. The best workaround for this is to call Webcam Start followed by Webcam Stop after connecting USB in order to make the webcam truly IDLE and thus willing to accept setting changes. Intermittent failure to connect to the cameras Wifi Access Point On rare occasions, connections to the camera’s Wifi AP will continuously fail until the camera is reset. It is possible to workaround this as described in Troubleshooting Spurious Protobuf Notifications sent once camera is connected in Station mode Once the camera has been connected in station mode (STA), it will start sending protobuf notifications with action ID 0xFF. These should be ignored. Hero 11 (v01.10.00) Specific Wired Communication is broken after update mode This is fixed by Resetting Connections and then re-pairing. Hero 13 (v01.10.00) Specific Webcam endpoints are broken. The following endpoints will always return 500 error status: Start Webcam Exit Webcam Preview Webcam", + "excerpt": "Frequently Asked Questions (FAQ) If you have somehow stumbled here first, note that there are specifications, demos, and tutorials which expand upon much of the information here. These can be found, among other places, from the home page. Connectivity What is the distance from the camera that BLE will still work? It is standard Bluetooth 4.0 range and it depends on external factors such as: Interference: anything interfering with the signal will shorten the range. The type of device that the camera is connected to: BT classification distinguishes 3 device classes based on their power levels. Depending on the class of the connected device, the range varies from less than 10 meters to 100 meters. Can I connect using WiFi only? Theoretically yes, if you already know the SSID, password, and the camera’s WiFi AP has been enabled. However, practically no because BLE is required in order to discover this information and configure the AP. Can I connect using BLE only? Yes, however there is some functionality that is not possible over BLE such as accessing the media list and downloading files. How to allow third-party devices to automatically discover close-by GoPro cameras? Devices can only be discovered via BLE by scanning for advertising GoPro cameras Multi Camera Setups How many devices can connect to the camera? Simultaneously, only one device can connect at a time. However, the camera stores BLE security keys and other connection information so it is possible to connect multiple devices sequentially. Is there currently a way to connect multiple cameras on the same Wifi network? No. Cameras can only be connected through Wi-Fi by becoming an access point itself (generating its own Wi-Fi network), not as a peripheral. What is the time offset between multiple cameras executing the same command? In cases when camera sync is important, we recommend using the USB connection, which minimizes the variance among devices. The time drift among cameras connected by USB cable to the same host will be up to ~35ms. Using BLE for that purpose will further increase it. Is there a way to precisely time sync cameras so the footage can be aligned during post-processing? The cameras set their time via GPS. By default, the camera seeks GPS at the beginning of every session, but this can be hindered by typical limitations of GPS signals. Additionally, there are two advanced options that require GoPro Labs firmware installed on the camera. The best bet is multi-cam GPS sync. Another option is precise time calibration via a dynamic QR scan from a smartphone or PC. Streaming What are the differences between the streaming options for GoPros? There are currently 3 different options on how to stream out of GoPro cameras. They are available either via Wi-Fi, USB, or both.   : Wifi :   : USB :       ViewFinder Preview LiveStream ViewFinder Preview Webcam Preview Webcam Orientation Floating or Fixed Landscape facing up Floating or Fixed Landscape: Floating or Fixed Landscape: Floating or Fixed Streaming Protocols UDP (MPEG-TS) RTMP UDP (MPEG-TS) UDP (MPEG-TS) UDP (MPEG-TS) \\         RTSP RTSP Connection Protocol Wifi - AP Mode WiFi - STA Mode NCM NCM NCM Resolution 480p, 720p 480p, 720p, 1080p 480p, 720p 720p, 1080p 720p, 1080p Frame Rate 30 30 30 30 30 Bitrate 2.5 - 4 mbps 0.8 - 8 mbps 2.5 - 4 mbps 6 mbps 6 mbps \\   depending on model configurable depending on model     Stabilization Basic Stabilization HyperSmooth or none Basic Stabilization None None Image Quality Basic Same as recorded content Basic Basic Same as recorded content Minimum Latency 210 ms > 100ms (un-stabilized) 210 ms 210 ms 210 ms \\     > 1,100ms (stabilized)       Audio None Stereo None None None Max Stream Time 150 minutes (720p on fully 85 minutes (720p on fully Unlimited (with external Unlimited(with external Unlimited (with external\\   charged Enduro battery) charged Enduro battery) power via USB) power via USB) power via USB How to achieve low latency feed streaming onto displays? The stream has a minimum latency of about 210ms. If you are seeing latency higher than that, we often find that as a result of using off-the-shelf libraries like ffmpeg which adds its own buffering. For preview and framing use cases, we don’t recommend using the live streaming RTMP protocol because it adds unnecessary steps in the pipeline, and puts the camera in the streaming preset, which offers little other control. A low latency streaming demo is available in the demos. How do I minimize latency of video preview with FFPLAY? FFPLAY by default will buffer remote streams. You can minimize the buffer using: --no-cache (receiving side) `-fflags nobuffer” (sender). However, the best latency can be achieved by building your own pipeline or ffmpegs library for decoding the bytes. Users should be able to achieve about 200-300 ms latency on the desktop and possibly optimize lower than that. How to view the video stream in VLC? To view the stream in VLC, you need to open network stream udp://@0.0.0.0:8554. You will still see latency because VLC uses its own caching. Power What are the power requirements for GoPro if connected over USB? All cameras have minimum power requirements, as specified here. As long as the power is supplied, cameras will be fully functional with or without an internal battery. Removing the battery and running on USB power alone will improve thermal performance and runtime. If you are seeing issues with insufficient power and have a charger with correct specs, the problems likely stem from low quality cables or low-quality adapters that are not able to consistently provide advertised amperage. We have encountered USB-C cables manufactured with poor quality control that transfer enough power only when their connectors face one side up, but not the other. We recommend using only high-quality components that deliver the correct power output How to enable automatic power on and off in wired setups? Cameras cannot be switched on remotely over USB, or “woken up” remotely after they “go to sleep”. The best workaround for this is via the GoPro Labs firmware that forces the camera to automatically switch on as soon as it detects USB power and switch off when the powering stops. Refer to the WAKE command here. Metadata Can I use the GPS track from the camera in real time? No. The GPS track on the camera as well as other metadata is not available until the file is written and saved. If the objective is to add metadata to the stream, currently the only option is to pull GPS data from another device (phone, wearable,… ) and sync it to the video feed. What can be accessed in the metadata file? Metadata exists as a proprietary GPMF (GoPro Metadata Format) and can be extracted from the file via API commands separately for GPS, Telemetry data, or the entire metadata container. The following data points can be extracted: Camera settings (Exposure time, ISO, Sensor Gain, White balance) Date and Time IMU: GPS, gyroscope, and accelerometer Smile detection Audio levels Face detection in bounding boxes Scene Classifiers (water, urban, vegetation, snow, beach, indoor) Is there a way to change the file names or otherwise classify my video file? Currently there are two options to do that, and both require GoPro Labs firmware. The stock firmware doesn’t provide that option. With GoPro Labs installed, you can either inject metadata into the file (and extract it later with the GPMF parser) or use custom naming for the file. Is there a way to add time stamps to the video files and mark specific moments? Open GoPro users can add time stamped markers, called “Hilights”, to flag specific moments in the video. Hilights can be injected into the video in the real time and then extracted for analytics or other post-processing purposes. The same Hilights are used in GoPro’s auto-editing engine Quik to determine the most interesting moments in the video. General Which cameras are supported by Open GoPro? The answer at a high level is >= Hero 9. However, there are also certain firmware requirements. For a complete answer, see the Specification. How to get the remaining timelapse capability? First check the value of Setting 128. Then depending on whether this is Photo or Video, use: Status 34 (Remaining photos) Status 35 (Remaining videos) Camera Logic Do commands operate as priority-wise or time-related? The cameras use first-in, first-out logic. Is there an option to send the commands in cyclic format instead of sending requests for each command? If you want to receive information asynchronously, it is possible via registering for BLE notifications. See an example (tracking battery) in the Python SDK. Troubleshooting If you are able to consistently reproduce a problem, please file a bug on Github Issues Why is the camera not advertising? If you have not yet paired to the camera with the desired device, then you need to first set the camera into pairing mode (Connections->Connect Device->Quick App). If you have already paired, then the camera should be advertising and ready to connect. If it is not advertising, it is possible you are already connected to it from a previous session. To be sure, power cycle both the camera and the peer device. Workaround for intermittent Wifi AP Connection failure On >= Hero 11, try disabling and then re-enabling the camera’s Wifi AP using the AP Control BLE Command Known Issues Relevant to All Supported Cameras Webcam does not enter idle mode once plugged in The webcam status will be wrongly reported as IDLE instead of OFF after a new USB connection. The best workaround for this is to call Webcam Start followed by Webcam Stop after connecting USB in order to make the webcam truly IDLE and thus willing to accept setting changes. Intermittent failure to connect to the cameras Wifi Access Point On rare occasions, connections to the camera’s Wifi AP will continuously fail until the camera is reset. It is possible to workaround this as described in Troubleshooting Spurious Protobuf Notifications sent once camera is connected in Station mode Once the camera has been connected in station mode (STA), it will start sending protobuf notifications with action ID 0xFF. These should be ignored. Hero 11 (v01.10.00) Specific Wired Communication is broken after update mode This is fixed by Resetting Connections and then re-pairing. Hero 13 (v01.10.00) Specific Webcam endpoints are broken. The following endpoints will always return 500 error status: Start Webcam Exit Webcam Preview Webcam Camera is not discoverable via MDNS. The camera does not advertise the _gopro-web service.", "categories": [], "tags": [], "url": "/OpenGoPro/faq#" @@ -36,14 +36,14 @@ var jekyllStore = [ }, { "title": "Tutorial 3: Parse BLE TLV Responses: ", - "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to parse BLE Type-Length-Value (TLV) Responses. Besides TLV, some BLE operations instead return protobuf responses. These are not considered here and will be discussed in a future tutorial 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 are present and configured correctly. It is suggested that you have first completed the connect and sending commands tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 3 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Parsing a One Packet TLV Response You can test parsing a one packet TLV response with your camera through BLE using the following script: $ python ble_command_get_version.py See the help for parameter definitions: $ python ble_command_get_version.py --help usage: ble_command_get_version.py [-h] [-i IDENTIFIER] Connect to a GoPro camera via BLE, then get the Open GoPro version. 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 Parsing Multiple Packet TLV Responses You can test parsing multiple packet TVL responses with your camera through BLE using the following script: $ python ble_command_get_hardware_info.py See the help for parameter definitions: $ 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 hardware info. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 3” 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. Perform Tutorial 3 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. Setup We must first connect as was discussed in the connecting BLE tutorial. 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 performing specific operations where this works and we know that the sequence always appears as such (connection sequence left out for brevity): GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start)) In actuality, responses can be more complicated. As described in the BLE Spec, responses can be be comprised of multiple packets where each packet is <= 20 bytes such as: GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation)) This requires the implementation of accumulating and parsing algorithms which will be described below. 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) Operation ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes 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 Set Shutter. It returned a response of: 02:01:00 This equates to: 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. Responses with Payload 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: Response With Payload In this tutorial, we will walk through creating a simple parser to parse the Open GoPro Get Version Command which is an example of such an operation. 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. First, we send the Get Version Command to the Command Request UUID in the same manner as commands were sent in the previous tutorial: python kotlin 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 receive a response at the expected handle (as a TLV Response). This is logged as: 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 val versionRequest = ubyteArrayOf(0x01U, 0x51U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, versionRequest) var tlvResponse = receivedResponses.receive() as Response.Tlv We then receive a response at the expected handle. This is logged as: This is logged as such: 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 CQ_COMMAND_RSP Received packet of length 6. 0 bytes remaining This response equates to: 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 response payload contains 4 additional bytes that need to be parsed. Using the information from the Get Version Documentation, we know to parse this as: 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 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. python kotlin The snippets of code included in this section are taken from the notification handler 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] The snippets of code included in this section are taken from the Response.Tlv.Parse method // Parse header bytes tlvResponse.parse() ... open fun parse() { require(isReceived) id = rawBytes[0].toInt() status = rawBytes[1].toInt() // Store remainder as payload payload = rawBytes.drop(2).toUByteArray() } 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: python kotlin 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: Received a response to command_id=81 with status=0, payload=01:02:01:00 The version is Open GoPro 2.0 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. 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: Received response: ID: 81, Status: 0, Payload: 01:02:01:00 Got the Open GoPro version successfully: 2.0 Quiz time! 📚 ✏️ What is the maximum size of an individual notification response packet at the Open GoPro application layer? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. Responses can be composed of multiple packets where each packet is at maximum 20 bytes. What is the maximum amount of bytes that one response can be composed of? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There is no limit on the amount of packets that can comprise a response. How many packets are command responses composed of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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. How many packets are setting responses comprised of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. 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. 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 headers as they are defined, reproduced for reference here: Byte 1 Byte 2 (optional) Byte 3 (optional) 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 0: Start 00: General Message Length: 5 bits 0: Start 01: Extended (13-bit) Message Length: 13 bits 0: Start 10: Extended (16-bit) Message Length: 16 bits 0: Start 11: Reserved 1: Continuation The basic accumulation algorithm (which is implemented in the Response.Accumulate method) is as follows: Is the continuation bit set? python kotlin The example script that will be walked through for this section is ble_command_get_hardware_info.py. if buf[0] & CONT_MASK: buf.pop(0) else: ... 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 ... No, the continuation bit was not set. Therefore create new response, then get its length. python kotlin 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:] // This is a new packet so start with empty array packet = 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\") } } Append current packet to response and decrement bytes remaining. python kotlin Append payload to buffer and update remaining / complete self.bytes.extend(buf) self.bytes_remaining -= len(buf) // Accumulate the payload now that headers are handled and dropped packet += buf bytesRemaining -= buf.size In the notification handler, we are then enqueueing the received response if there are no bytes remaining. python kotlin if response.is_received: ... 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: 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() if (response.isReceived) { if (uuid == GoProUUID.CQ_COMMAND_RSP) { CoroutineScope(Dispatchers.IO).launch { receivedResponses.send(response) } } ... NoYesDecrement bytes remainingYesNoRead Available PacketContinuation bit set?Create new empty responseGet bytes remaining, i.e. lengthAppend packet to accumulating responseBytes remaining == 0?Parse Received Packet We can see this in action when we send the Get Hardware Info Command: python kotlin 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() val hardwareInfoRequest = ubyteArrayOf(0x01U, 0x3CU) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, hardwareInfoRequest) 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. This notification handler is only designed to handle TlvResponses. This is fine for this tutorial since that is all we will be receiving. python kotlin 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() 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) 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) private fun notificationHandler(characteristic: UUID, data: UByteArray) { ... 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) } } } We can see the individual packets being accumulated in the log: python kotlin 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 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 At this point the response has been accumulated. We then parse and log the payload using the Get Hardware Info response documentation: python kotlin hardware_info = HardwareInfo.from_bytes(response.payload) logger.info(f\"Received hardware info: {hardware_info}\") where the parsing is done as such: @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: 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\" } tlvResponse.parse() val hardwareInfo = HardwareInfo.fromBytes(tlvResponse.payload) where the parsing is done as such: 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) } This logs as: Got the Hardware Info successfully: HardwareInfo( modelNumber=1040187392, modelName=HERO12 Black, firmwareVersion=H23.01.01.99.56, serialNumber=C3501324500702, apSsid=GP24500702, apMacAddress=2674f7f66104 ) Quiz time! 📚 ✏️ How can we know that a response has been completely received? A: The stop bit will be set in the header B: The response has accumulated length bytes C: By checking for the end of frame (EOF) sentinel character Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. 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 See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 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. To learn about a different type of operation (Queries), go to the next tutorial.", + "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to parse BLE Type-Length-Value (TLV) Responses. Besides TLV, some BLE operations instead return protobuf responses. These are not considered here and will be discussed in a future tutorial 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 are present and configured correctly. It is suggested that you have first completed the connect and sending commands tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 3 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Parsing a One Packet TLV Response You can test parsing a one packet TLV response with your camera through BLE using the following script: $ python ble_command_get_version.py See the help for parameter definitions: $ python ble_command_get_version.py --help usage: ble_command_get_version.py [-h] [-i IDENTIFIER] Connect to a GoPro camera via BLE, then get the Open GoPro version. 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 Parsing Multiple Packet TLV Responses You can test parsing multiple packet TVL responses with your camera through BLE using the following script: $ python ble_command_get_hardware_info.py See the help for parameter definitions: $ 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 hardware info. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 3” 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. Perform Tutorial 3 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. Setup We must first connect as was discussed in the connecting BLE tutorial. 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 performing specific operations where this works and we know that the sequence always appears as such (connection sequence left out for brevity): GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start)) In actuality, responses can be more complicated. As described in the BLE Spec, responses can be be comprised of multiple packets where each packet is <= 20 bytes such as: GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation))Notification Response (MSB == 1 (continuation)) This requires the implementation of accumulating and parsing algorithms which will be described below. 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) Operation ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes 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 Set Shutter. It returned a response of: 02:01:00 This equates to: 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. Responses with Payload 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: Response With Payload In this tutorial, we will walk through creating a simple parser to parse the Open GoPro Get Version Command which is an example of such an operation. 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. First, we send the Get Version Command to the Command Request UUID in the same manner as commands were sent in the previous tutorial: python kotlin 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 receive a response at the expected handle (as a TLV Response). This is logged as: 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 val versionRequest = ubyteArrayOf(0x01U, 0x51U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, versionRequest) var tlvResponse = receivedResponses.receive() as Response.Tlv We then receive a response at the expected handle. This is logged as: This is logged as such: 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 CQ_COMMAND_RSP Received packet of length 6. 0 bytes remaining This response equates to: 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 response payload contains 4 additional bytes that need to be parsed. Using the information from the Get Version Documentation, we know to parse this as: 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 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. python kotlin The snippets of code included in this section are taken from the notification handler 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] The snippets of code included in this section are taken from the Response.Tlv.Parse method // Parse header bytes tlvResponse.parse() ... open fun parse() { require(isReceived) id = rawBytes[0].toInt() status = rawBytes[1].toInt() // Store remainder as payload payload = rawBytes.drop(2).toUByteArray() } 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: python kotlin 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: Received a response to command_id=81 with status=0, payload=01:02:01:00 The version is Open GoPro 2.0 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. 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: Received response: ID: 81, Status: 0, Payload: 01:02:01:00 Got the Open GoPro version successfully: 2.0 Quiz time! 📚 ✏️ What is the maximum size of an individual notification response packet at the Open GoPro application layer? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. Responses can be composed of multiple packets where each packet is at maximum 20 bytes. What is the maximum amount of bytes that one response can be composed of? A: 20 bytes B: 256 bytes C: There is no maximum size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There is no limit on the amount of packets that can comprise a response. How many packets are command responses composed of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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. How many packets are setting responses comprised of? A: Always 1 packet B: Always multiple packets. C: A variable amount of packets depending on the payload size Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. 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. 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 headers as they are defined, reproduced for reference here: Byte 1 Byte 2 (optional) Byte 3 (optional) 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 0: Start 00: General Message Length: 5 bits 0: Start 01: Extended (13-bit) Message Length: 13 bits 0: Start 10: Extended (16-bit) Message Length: 16 bits 0: Start 11: Reserved 1: Continuation The basic accumulation algorithm (which is implemented in the Response.Accumulate method) is as follows: Is the continuation bit set? python kotlin The example script that will be walked through for this section is ble_command_get_hardware_info.py. if buf[0] & CONT_MASK: buf.pop(0) else: ... 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 ... No, the continuation bit was not set. Therefore create new response, then get its length. python kotlin 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:] // This is a new packet so start with empty array packet = 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\") } } Append current packet to response and decrement bytes remaining. python kotlin Append payload to buffer and update remaining / complete self.bytes.extend(buf) self.bytes_remaining -= len(buf) // Accumulate the payload now that headers are handled and dropped packet += buf bytesRemaining -= buf.size In the notification handler, we are then enqueueing the received response if there are no bytes remaining. python kotlin if response.is_received: ... 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: 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() if (response.isReceived) { if (uuid == GoProUUID.CQ_COMMAND_RSP) { CoroutineScope(Dispatchers.IO).launch { receivedResponses.send(response) } } ... No Yes Decrement bytes remaining Yes No Read Available Packet Continuation bit set? Create new empty response Get bytes remaining, i.e. length Append packet to accumulating response Bytes remaining == 0? Parse Received Packet We can see this in action when we send the Get Hardware Info Command: python kotlin 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() val hardwareInfoRequest = ubyteArrayOf(0x01U, 0x3CU) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, hardwareInfoRequest) 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. This notification handler is only designed to handle TlvResponses. This is fine for this tutorial since that is all we will be receiving. python kotlin 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() 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) 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) private fun notificationHandler(characteristic: UUID, data: UByteArray) { ... 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) } } } We can see the individual packets being accumulated in the log: python kotlin 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 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 At this point the response has been accumulated. We then parse and log the payload using the Get Hardware Info response documentation: python kotlin hardware_info = HardwareInfo.from_bytes(response.payload) logger.info(f\"Received hardware info: {hardware_info}\") where the parsing is done as such: @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: 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\" } tlvResponse.parse() val hardwareInfo = HardwareInfo.fromBytes(tlvResponse.payload) where the parsing is done as such: 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) } This logs as: Got the Hardware Info successfully: HardwareInfo( modelNumber=1040187392, modelName=HERO12 Black, firmwareVersion=H23.01.01.99.56, serialNumber=C3501324500702, apSsid=GP24500702, apMacAddress=2674f7f66104 ) Quiz time! 📚 ✏️ How can we know that a response has been completely received? A: The stop bit will be set in the header B: The response has accumulated length bytes C: By checking for the end of frame (EOF) sentinel character Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. 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 See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 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. To learn about a different type of operation (Queries), go to the next tutorial.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/parse-ble-responses#" }, { "title": "Tutorial 4: BLE TLV Queries: ", - "excerpt": "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 operations that are initiated by writing to the Query UUID and receiving responses via the Query Response UUID. A list of queries can be found in the Query ID Table. It is important to distinguish between queries and commands because they each have different request and response packet formats. 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. Requirements It is assumed that the hardware and software requirements from the connecting BLE tutorial are present and configured correctly. It is suggested that you have first completed the connect, sending commands, and parsing responses tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 4 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Individual Query Poll You can test an individual query poll with your camera through BLE using the following script: $ python ble_query_poll_resolution_value.py See the help for parameter definitions: $ 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. 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 Multiple Simultaneous Query Polls You can test querying multiple queries simultaneously with your camera through BLE using the following script: $ python ble_query_poll_multiple_setting_values.py See the help for parameter definitions: $ 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. 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 Registering for Query Push Notifications You can test registering for querties and receiving push notifications with your camera through BLE using the following script: $ python ble_query_register_resolution_value_updates.py See the help for parameter definitions: $ 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. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 4” 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. Perform Tutorial 4 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. Setup We must first connect as was discussed in the connecting BLE tutorial. python kotlin 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. 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: 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) The code above is taken from ble_query_poll_resolution_value.py We are defining a resolution enum that will be updated as we receive new resolutions: private enum class Resolution(val value: UByte) { RES_4K(1U), RES_2_7K(4U), RES_2_7K_4_3(6U), RES_1080(9U), RES_4K_4_3(18U), RES_5K(24U); companion object { private val valueMap: Map<UByte, Resolution> by lazy { values().associateBy { it.value } } fun fromValue(value: UByte) = valueMap.getValue(value) } } private lateinit var resolution: Resolution There are two methods to query status / setting information, each of which will be described in a following section: Polling Query Information 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 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. python kotlin 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:] 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) } yesnoParse Query IDParse StatusMore data?Get Value IDGet Value LengthGet Valuedone How many packets are query responses? A: Always 1 packet B: Always multiple packets C: Can be 1 or multiple packets Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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). Which field is not common to all TLV responses? A: length B: status C: ID D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. 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 queries: Query ID Request Query 0x12 [Get Setting value(s)](/OpenGoPro/ble/features/query.htmlget-setting-values) len:12:xx:xx 0x13 [Get Status value(s)](/OpenGoPro/ble/features/query.htmlget-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. Since they are two separate queries, combination of settings / statuses can not be polled simultaneously. Here is a generic sequence diagram (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)Get Setting value(s) queries written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUID The number of notification responses will vary depending on the amount of settings that have been queried. 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 Individual Query Poll Here we will walk through an example of polling one setting (Resolution). First we send the query: python kotlin The sample code can be found in in ble_query_poll_resolution_value.py. 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) val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) 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. python kotlin Wait to receive the notification response response = await received_responses.get() response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) which logs as such: 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 // Wait to receive the response and then convert it to resolution val queryResponse = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) which logs as such: 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:09 Received response on CQ_QUERY_RSP Received packet of length 5. 0 bytes remaining Received Query Response Camera resolution is RES_1080 For verification purposes, we are then changing the resolution and polling again to verify that the setting has changed: python kotlin 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: 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... 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: 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 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: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_2_7K 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 that the query now includes 3 settings: Resolution, FPS, and FOV. python kotlin RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 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 TODO The length (first byte of the query) has been increased to 4 to accommodate the extra settings We are also parsing the response to get all 3 values: python kotlin 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])}\") TODO When we are storing the updated setting, we are just taking the first byte (i..e index 0). A real-world implementation would need to know the length (and type) of the setting / status response by the ID. For example, sometimes settings / statuses are bytes, words, strings, etc. They are then printed to the log which will look like the following: python kotlin 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 TODO In general, we can parse query values by looking at relevant documentation linked from the Setting or Status ID tables. For example (for settings): ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 Query All 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 Quiz time! 📚 ✏️ How can we poll the encoding status and the resolution setting using one query? A: Concatenate a &8216;Get Setting Value&8217; query and a &8216;Get Status&8217; query with the relevant ID&8217;s B: Concatenate the &8216;Get All Setting&8217; and &8216;Get All Status&8217; queries. C: It is not possible Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. It is not possible to concatenate queries. This would result in an unknown sequence of bytes from the camera&8217;s perspective. So it is not possible to get a setting value and a status value in one 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. Registering for Query Push Notifications 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 queries are: Query ID Request Query 0x52 [Register updates for setting(s)](/OpenGoPro/ble/features/query.htmlregister-for-setting-value-updates) len:52:xx:xx 0x53 [Register updates for status(es)](/OpenGoPro/ble/features/query.htmlregister-for-status-value-updates) len:53:xx:xx 0x72 [Unregister updates for setting(s)](/OpenGoPro/ble/features/query.htmlunregister-for-setting-value-updates) len:72:xx:xx 0x73 [Unregister updates for status(es)](/OpenGoPro/ble/features/query.htmlunregister-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: Query ID Response 0x92 Setting Value Push Notification 0x93 Status Value Push Notification Here is a generic sequence diagram of how this looks (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)loop[Setting changes]loop[Settingchanges]Register updates for settingNotification Response and Current Setting ValueSetting changesPush notification of new setting valueUnregister updates for settingNotification ResponseSetting changes That is, after registering for push notifications for a given query, notification responses will continuously be sent whenever the query changes until the client unregisters for push notifications for the given query. The initial response to the Register query also contains the current setting / status value. We will walk through an example of this below: First, let’s register for updates when the resolution setting changes: python kotlin 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() val registerResolutionUpdates = ubyteArrayOf(0x02U, 0x52U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, registerResolutionUpdates) 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. python kotlin response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) logger.info(f\"Resolution is currently {resolution}\") This will show in the log as such: 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 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: 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 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 and waiting to receive the update. notification.. python kotlin 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: 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... 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 = (receivedResponses.receive() as Response.Tlv).apply { parse() } // Verify we receive the update from the camera when the resolution changes while (resolution != targetResolution) { val queryNotification = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) } We can see change happen in the log: 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 Resolution successfully changed Waiting for camera to inform us about the resolution change Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:92:00:02:01:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:92:00:02:01:04 Received resolution query response Resolution is now RES_2_7K In this case, the Query ID is 0x92 (Setting Value Push Notification) as expected. Multiple push notifications can be registered / received in a similar manner that multiple queries were polled above Quiz time! 📚 ✏️ True or False: We can still poll a given query value while we are currently registered to receive push notifications for it. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. While there is probably not a good reason to do so, there is nothing preventing polling in this manner. True or False: A push notification for a registered setting will only ever contain query information about one setting ID. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. It is possible for push notifications to contain multiple setting ID&8217;s if both setting ID&8217;s have push notifications registered and both settings change at the same time. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now query any of the settings / statuses from the camera using one of the above patterns.", + "excerpt": "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 operations that are initiated by writing to the Query UUID and receiving responses via the Query Response UUID. A list of queries can be found in the Query ID Table. It is important to distinguish between queries and commands because they each have different request and response packet formats. 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. Requirements It is assumed that the hardware and software requirements from the connecting BLE tutorial are present and configured correctly. It is suggested that you have first completed the connect, sending commands, and parsing responses tutorials before going through this tutorial. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 4 directory. Python >= 3.9 and < 3.12 must be used as specified in the requirements Individual Query Poll You can test an individual query poll with your camera through BLE using the following script: $ python ble_query_poll_resolution_value.py See the help for parameter definitions: $ 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. 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 Multiple Simultaneous Query Polls You can test querying multiple queries simultaneously with your camera through BLE using the following script: $ python ble_query_poll_multiple_setting_values.py See the help for parameter definitions: $ 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. 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 Registering for Query Push Notifications You can test registering for querties and receiving push notifications with your camera through BLE using the following script: $ python ble_query_register_resolution_value_updates.py See the help for parameter definitions: $ 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. 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 The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 4” 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. Perform Tutorial 4 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. Setup We must first connect as was discussed in the connecting BLE tutorial. python kotlin 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. 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: 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) The code above is taken from ble_query_poll_resolution_value.py We are defining a resolution enum that will be updated as we receive new resolutions: private enum class Resolution(val value: UByte) { RES_4K(1U), RES_2_7K(4U), RES_2_7K_4_3(6U), RES_1080(9U), RES_4K_4_3(18U), RES_5K(24U); companion object { private val valueMap: Map<UByte, Resolution> by lazy { values().associateBy { it.value } } fun fromValue(value: UByte) = valueMap.getValue(value) } } private lateinit var resolution: Resolution There are two methods to query status / setting information, each of which will be described in a following section: Polling Query Information 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 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. python kotlin 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:] 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) } yes no Parse Query ID Parse Status More data? Get Value ID Get Value Length Get Value done How many packets are query responses? A: Always 1 packet B: Always multiple packets C: Can be 1 or multiple packets Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. 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). Which field is not common to all TLV responses? A: length B: status C: ID D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. 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 queries: Query ID Request Query 0x12 [Get Setting value(s)](/OpenGoPro/ble/features/query.htmlget-setting-values) len:12:xx:xx 0x13 [Get Status value(s)](/OpenGoPro/ble/features/query.htmlget-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. Since they are two separate queries, combination of settings / statuses can not be polled simultaneously. Here is a generic sequence diagram (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)Get Setting value(s) queries written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUID The number of notification responses will vary depending on the amount of settings that have been queried. 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 Individual Query Poll Here we will walk through an example of polling one setting (Resolution). First we send the query: python kotlin The sample code can be found in in ble_query_poll_resolution_value.py. 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) val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) 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. python kotlin Wait to receive the notification response response = await received_responses.get() response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) which logs as such: 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 // Wait to receive the response and then convert it to resolution val queryResponse = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) which logs as such: 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:09 Received response on CQ_QUERY_RSP Received packet of length 5. 0 bytes remaining Received Query Response Camera resolution is RES_1080 For verification purposes, we are then changing the resolution and polling again to verify that the setting has changed: python kotlin 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: 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... 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: 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 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: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_2_7K 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 that the query now includes 3 settings: Resolution, FPS, and FOV. python kotlin RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 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 TODO The length (first byte of the query) has been increased to 4 to accommodate the extra settings We are also parsing the response to get all 3 values: python kotlin 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])}\") TODO When we are storing the updated setting, we are just taking the first byte (i..e index 0). A real-world implementation would need to know the length (and type) of the setting / status response by the ID. For example, sometimes settings / statuses are bytes, words, strings, etc. They are then printed to the log which will look like the following: python kotlin 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 TODO In general, we can parse query values by looking at relevant documentation linked from the Setting or Status ID tables. For example (for settings): ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 Query All 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 Quiz time! 📚 ✏️ How can we poll the encoding status and the resolution setting using one query? A: Concatenate a &8216;Get Setting Value&8217; query and a &8216;Get Status&8217; query with the relevant ID&8217;s B: Concatenate the &8216;Get All Setting&8217; and &8216;Get All Status&8217; queries. C: It is not possible Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. It is not possible to concatenate queries. This would result in an unknown sequence of bytes from the camera&8217;s perspective. So it is not possible to get a setting value and a status value in one 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. Registering for Query Push Notifications 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 queries are: Query ID Request Query 0x52 [Register updates for setting(s)](/OpenGoPro/ble/features/query.htmlregister-for-setting-value-updates) len:52:xx:xx 0x53 [Register updates for status(es)](/OpenGoPro/ble/features/query.htmlregister-for-status-value-updates) len:53:xx:xx 0x72 [Unregister updates for setting(s)](/OpenGoPro/ble/features/query.htmlunregister-for-setting-value-updates) len:72:xx:xx 0x73 [Unregister updates for status(es)](/OpenGoPro/ble/features/query.htmlunregister-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: Query ID Response 0x92 Setting Value Push Notification 0x93 Status Value Push Notification Here is a generic sequence diagram of how this looks (the same is true for statuses): GoProOpen GoPro user deviceGoProOpen GoPro user deviceConnected (steps from connect tutorial)loop[Setting changes]loop[Settingchanges]Register updates for settingNotification Response and Current Setting ValueSetting changesPush notification of new setting valueUnregister updates for settingNotification ResponseSetting changes That is, after registering for push notifications for a given query, notification responses will continuously be sent whenever the query changes until the client unregisters for push notifications for the given query. The initial response to the Register query also contains the current setting / status value. We will walk through an example of this below: First, let’s register for updates when the resolution setting changes: python kotlin 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() val registerResolutionUpdates = ubyteArrayOf(0x02U, 0x52U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, registerResolutionUpdates) 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. python kotlin response.parse() resolution = Resolution(response.data[RESOLUTION_ID][0]) logger.info(f\"Resolution is currently {resolution}\") This will show in the log as such: 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 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: 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 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 and waiting to receive the update. notification.. python kotlin 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: 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... 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 = (receivedResponses.receive() as Response.Tlv).apply { parse() } // Verify we receive the update from the camera when the resolution changes while (resolution != targetResolution) { val queryNotification = (receivedResponses.receive() as Response.Query).apply { parse() } resolution = Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) } We can see change happen in the log: 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 Resolution successfully changed Waiting for camera to inform us about the resolution change Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:92:00:02:01:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:92:00:02:01:04 Received resolution query response Resolution is now RES_2_7K In this case, the Query ID is 0x92 (Setting Value Push Notification) as expected. Multiple push notifications can be registered / received in a similar manner that multiple queries were polled above Quiz time! 📚 ✏️ True or False: We can still poll a given query value while we are currently registered to receive push notifications for it. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. While there is probably not a good reason to do so, there is nothing preventing polling in this manner. True or False: A push notification for a registered setting will only ever contain query information about one setting ID. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. It is possible for push notifications to contain multiple setting ID&8217;s if both setting ID&8217;s have push notifications registered and both settings change at the same time. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now query any of the settings / statuses from the camera using one of the above patterns.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/ble-queries#" diff --git a/contribution.html b/contribution.html index 55099e5b..339d2890 100644 --- a/contribution.html +++ b/contribution.html @@ -521,10 +521,10 @@

Quiz

%} -
+
-
What is the question?
-
+
What is the question?
+

@@ -537,10 +537,10 @@

Quiz

- - @@ -556,10 +556,10 @@

Quiz

%}
-
+
-
True or False?
-
+
True or False?
+

@@ -569,10 +569,10 @@

Quiz

- - @@ -595,7 +595,7 @@

Tabs

-
    +
    • tab1 @@ -610,7 +610,7 @@

      Tabs

    -
      +
      • This is the content of the first tab.

        @@ -649,7 +649,7 @@

        Mermaid

- + 76%17%7%Pets adopted by volunteersDogsCatsRats

PlantUML

diff --git a/faq.html b/faq.html index b1361732..64910572 100644 --- a/faq.html +++ b/faq.html @@ -841,6 +841,7 @@

Hero 11 (v01.10.00) Specific

Hero 13 (v01.10.00) Specific

+
@@ -853,6 +854,14 @@

Hero 13 (v01.10.00) Specific

+ +
+ +
+

The camera does not advertise the _gopro-web service.

+
+
+ diff --git a/feed.xml b/feed.xml index 2f7a8dd6..c8a0c5f2 100644 --- a/feed.xml +++ b/feed.xml @@ -1 +1 @@ -Jekyll2024-09-05T15:09:53-07:00https://gopro.github.io/OpenGoPro/feed.xmlOpen GoProOpen Source GoPro InterfaceGoPro \ No newline at end of file +Jekyll2024-09-09T13:50:54-07:00https://gopro.github.io/OpenGoPro/feed.xmlOpen GoProOpen Source GoPro InterfaceGoPro \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 969a951b..3fc75c67 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,39 +2,39 @@ https://gopro.github.io/OpenGoPro/tutorials/connect-ble -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/send-ble-commands -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/parse-ble-responses -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/ble-queries -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/ble-protobuf -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/connect-wifi -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/send-wifi-commands -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/camera-media-list -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/tutorials/cohn -2024-09-05T15:09:53-07:00 +2024-09-09T13:50:54-07:00 https://gopro.github.io/OpenGoPro/ble/features/access_points.html @@ -110,10 +110,10 @@ https://gopro.github.io/OpenGoPro/ble_2_0.html -2024-09-05T14:59:12-07:00 +2024-09-09T13:40:08-07:00 https://gopro.github.io/OpenGoPro/http_2_0.html -2024-09-05T14:59:12-07:00 +2024-09-09T13:40:08-07:00 diff --git a/tutorials/ble-protobuf.html b/tutorials/ble-protobuf.html index ce1080ca..0a1e3349 100644 --- a/tutorials/ble-protobuf.html +++ b/tutorials/ble-protobuf.html @@ -34,7 +34,7 @@ - + @@ -427,7 +427,7 @@

- +
@@ -524,7 +524,7 @@

Requirements

Just Show me the Demo(s)!!

-
    +
    • python @@ -535,7 +535,7 @@

      Just Show me the Demo(s)!!

    -
      +
      • Each of the scripts for this tutorial can be found in the Tutorial 5 @@ -678,7 +678,7 @@

        Protobuf Message Example

-
    +
    • python @@ -689,7 +689,7 @@

      Protobuf Message Example

    -
      +
      • from tutorial_modules import proto
        @@ -723,7 +723,7 @@ 

        Protobuf Message Example

        ResponseGeneric object.

        -
          +
          • python @@ -734,7 +734,7 @@

            Protobuf Message Example

          -
            +
            • response_bytes = proto.ResponseGeneric(result=proto.EnumResultGeneric.RESULT_SUCCESS).SerializeToString()
              @@ -810,7 +810,7 @@ 

              Protobuf Response Parser

              -
                +
                • python @@ -821,7 +821,7 @@

                  Protobuf Response Parser

                -
                  +
                  • @@ -858,7 +858,7 @@

                    Set Turbo Transfer

                    Feature ID, Action ID, and length bytes:

                    -
                      +
                      • python @@ -869,7 +869,7 @@

                        Set Turbo Transfer

                      -
                        +
                        • turbo_mode_request = bytearray(
                          @@ -896,7 +896,7 @@ 

                          Set Turbo Transfer

                          from the Set Turbo Mode Documentation: ResponseGeneric.

                          -
                            +
                            • python @@ -907,7 +907,7 @@

                              Set Turbo Transfer

                            -
                              +
                              • await client.write_gatt_char(request_uuid.value, turbo_mode_request, response=True)
                                @@ -969,7 +969,7 @@ 

                                Response Manager

                                parsing of all response types:

                                -
                                  +
                                  • python @@ -980,7 +980,7 @@

                                    Response Manager

                                  -
                                    +
                                    • @@ -1063,7 +1063,7 @@

                                      Examples of Each Response Type

                                      Now let’s perform operations that will demonstrate each response type:

                                      -
                                        +
                                        • python @@ -1074,7 +1074,7 @@

                                          Examples of Each Response Type

                                        -
                                          +
                                          • # TLV Command (Setting)
                                            @@ -1157,7 +1157,7 @@ 

                                            Good Job!

                                            -

                                            Updated:

                                            +

                                            Updated:

                                            diff --git a/tutorials/ble-queries.html b/tutorials/ble-queries.html index f17ff88a..5db9c42f 100644 --- a/tutorials/ble-queries.html +++ b/tutorials/ble-queries.html @@ -34,7 +34,7 @@ - + @@ -427,7 +427,7 @@

                                            - +
                                            @@ -521,7 +521,7 @@

                                            Requirements

                                            Just Show me the Demo(s)!!

                                            -
                                              +
                                              • python @@ -532,7 +532,7 @@

                                                Just Show me the Demo(s)!!

                                              -
                                                +
                                                • Each of the scripts for this tutorial can be found in the Tutorial 4 @@ -658,7 +658,7 @@

                                                  Setup

                                                  connecting BLE tutorial.

                                                  -
                                                    + -
                                                      +
                                                      • We have slightly updated the notification handler from the previous tutorial to handle a QueryResponse instead of @@ -820,7 +820,7 @@

                                                        Parsing a Query Response

                                                        storing each value in a hash map indexed by ID for later access.

                                                        -
                                                          +
                                                          • python @@ -831,7 +831,7 @@

                                                            Parsing a Query Response

                                                          -
                                                            +
                                                            • class QueryResponse(TlvResponse):
                                                              @@ -881,17 +881,17 @@ 

                                                              Parsing a Query Response




                                                              - - -
                                                              yes
                                                              no
                                                              Parse Query ID
                                                              Parse Status
                                                              More data?
                                                              Get Value ID
                                                              Get Value Length
                                                              Get Value
                                                              done
                                                              + + +

                                                              yes

                                                              no

                                                              Parse Query ID

                                                              Parse Status

                                                              More data?

                                                              Get Value ID

                                                              Get Value Length

                                                              Get Value

                                                              done

                                                              -
                                                              +
                                                              -
                                                              How many packets are query responses?
                                                              -
                                                              +
                                                              How many packets are query responses?
                                                              +

                                                              @@ -904,19 +904,19 @@

                                                              Parsing a Query Response

                                                              - - -
                                                              +
                                                              -
                                                              Which field is not common to all TLV responses?
                                                              -
                                                              +
                                                              Which field is not common to all TLV responses?
                                                              +

                                                              @@ -932,10 +932,10 @@

                                                              Parsing a Query Response

                                                              - - @@ -976,8 +976,8 @@

                                                              Polling Query Information

                                                              Here is a generic sequence diagram (the same is true for statuses):

                                                              -GoProOpen GoPro user deviceGoProOpen GoPro user device -Connected (steps from connect tutorial)Get Setting value(s) queries written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUID +GoProOpen GoPro user deviceGoProOpen GoPro user device +Connected (steps from connect tutorial)Get Setting value(s) queries written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUID

                                                              The number of notification responses will vary depending on the amount of settings that have been queried. Note that setting values will be combined into one notification until it reaches the maximum notification @@ -992,7 +992,7 @@

                                                              Individual Query Poll

                                                              First we send the query:

                                                              -
                                                                +
                                                                • python @@ -1003,7 +1003,7 @@

                                                                  Individual Query Poll

                                                                -
                                                                  +
                                                                  • @@ -1031,7 +1031,7 @@

                                                                    Individual Query Poll

                                                                    QueryResponse class and extract the new resolution value.

                                                                    -
                                                                      +
                                                                      • python @@ -1042,7 +1042,7 @@

                                                                        Individual Query Poll

                                                                      -
                                                                        +
                                                                        • # Wait to receive the notification response
                                                                          @@ -1089,7 +1089,7 @@ 

                                                                          Individual Query Poll

                                                                          has changed:

                                                                          -
                                                                            +
                                                                            • python @@ -1100,7 +1100,7 @@

                                                                              Individual Query Poll

                                                                            -
                                                                              +
                                                                              • while resolution is not target_resolution:
                                                                                @@ -1170,7 +1170,7 @@ 

                                                                                Multiple Simultaneous Query Polls

                                                                                FOV.

                                                                                -
                                                                                  +
                                                                                  • python @@ -1181,7 +1181,7 @@

                                                                                    Multiple Simultaneous Query Polls

                                                                                  -
                                                                                    +
                                                                                    • RESOLUTION_ID = 2
                                                                                      @@ -1209,7 +1209,7 @@ 

                                                                                      Multiple Simultaneous Query Polls

                                                                                      We are also parsing the response to get all 3 values:

                                                                                      -
                                                                                        +
                                                                                        • python @@ -1220,7 +1220,7 @@

                                                                                          Multiple Simultaneous Query Polls

                                                                                        -
                                                                                          +
                                                                                          • response.parse()
                                                                                            @@ -1247,7 +1247,7 @@ 

                                                                                            Multiple Simultaneous Query Polls

                                                                                            They are then printed to the log which will look like the following:

                                                                                            -
                                                                                              +
                                                                                              • python @@ -1258,7 +1258,7 @@

                                                                                                Multiple Simultaneous Query Polls

                                                                                              -
                                                                                                +
                                                                                                • Getting the current resolution, fps, and fov.
                                                                                                  @@ -1318,10 +1318,10 @@ 

                                                                                                  Query All

                                                                                                  Quiz time! 📚 ✏️

                                                                                                  -
                                                                                                  +
                                                                                                  -
                                                                                                  How can we poll the encoding status and the resolution setting using one query?
                                                                                                  -
                                                                                                  +
                                                                                                  How can we poll the encoding status and the resolution setting using one query?
                                                                                                  +

                                                                                                  @@ -1334,10 +1334,10 @@

                                                                                                  Query All

                                                                                                  - -