diff --git a/assets/js/lunr/lunr-store.js b/assets/js/lunr/lunr-store.js index c3102a27..73d6fc73 100644 --- a/assets/js/lunr/lunr-store.js +++ b/assets/js/lunr/lunr-store.js @@ -449,49 +449,49 @@ var store = [ }, { "title": "Tutorial 1: Connect BLE: ", - "excerpt": "This tutorial will provide a walk-through to connect to the GoPro camera via Bluetooth Low Energy (BLE). Requirements Hardware A GoPro camera that is supported by Open GoPro python kotlin One of the following systems: Windows 10, version 16299 (Fall Creators Update) or greater Linux distribution with BlueZ >= 5.43 OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 An Android Device supporting SDK >= 33 Software python kotlin Python >= 3.8.x must be installed. See this Python installation guide. Android Studio >= 2022.1.1 (Electric Eel) Overview / Assumptions python kotlin This tutorial will use bleak to control the OS’s Bluetooth Low Energy (BLE). The Bleak BLE controller does not currently support autonomous pairing for the BlueZ backend. So if you are using BlueZ (i.e. Ubuntu, RaspberryPi, etc.), you need to first pair the camera from the command line as shown in the BlueZ tutorial. There is work to add this feature and progress can be tracked on the Github Issue. The bleak module is based on asyncio which means that its awaitable functions need to be called from an async coroutine. In order to do this, all of the code below should be running in an async function. We accomplish this in the tutorial scripts by making main async as such: import asyncio async def main() -> None: Put our code here if __name__ == \"__main__\": asyncio.run(main()) These are stripped down Python tutorials that are only meant to show the basics. For a complete Python SDK that uses bleak as the backend as well as a cross-platform WiFi backend to easily write Python apps that control the GoPro, see the Open GoPro Python SDK This tutorial will provide a set of Kotlin tutorials to demonstrate Open GoPro Functionality. The tutorials are provided as a single Android Studio project targeted to run on an Android device. The tutorials are only concerned with application-level demonstrations of the Open GoPro API and therefore do not prioritize the following: UI: The tutorial project only contains a minimal UI to select, implement, and view logs for each tutorial Android architecture / best practices: the project architecture is designed to encapsulate Kotlin functionality to easily display per-tutorial functionality Android-specific requirements: permission handling, adapter enabling, etc are implemented in the project but not documented in the tutorials BLE / Wifi (HTTP) functionality: A simple BLE API is included in the project and will be touched upon in the tutorials. However, the focus of the tutorials is not on how the BLE API is implemented as a real project would likely use a third-party library for this such as Kable See the Punchthrough tutorials for Android BLE-Specific tutorials These tutorials assume familiarity and a base level of competence with: Android Studio Bluetooth Low Energy JSON HTTP Setup python kotlin This set of tutorials is accompanied by a Python package consisting of scripts separated by tutorial module. These can be found on Github. Once the Github repo has been cloned or downloaded to your local machine, the package can be installed as follows: Enter the python tutorials directory at $INSTALL/demos/python/tutorial/ where $INSTALL is the top level of the Open GoPro repo where it exists on your local machine Use pip to install the package (in editable mode in case you want to test out some changes): pip install -e . While it is out of the scope of this tutorial to describe, it is recommended to install the package in to a virtual environment in order to isolate system dependencies. You can test that installation was successful by viewing the installed package’s information: $ pip show open-gopro-python-tutorials Name: open-gopro-python-tutorials Version: 0.0.3 Summary: Open GoPro Python Tutorials Home-page: https://github.com/gopro/OpenGoPro Author: Tim Camise Author-email: gopro.com License: MIT Location: c:\\users\\tim\\gopro\\opengopro\\demos\\python\\tutorial Requires: bleak, requests Required-by: This set of tutorials is accompanied by an Android Studio project consisting of, among other project infrastructure, Kotlin files separated by tutorial module. The project can be found on Github. Once the Github repo has been cloned or downloaded to your local machine, open the project in Android studio. At this point you should be able to build and load the project to your Android device. The project will not work on an emulated device since BLE can not be emulated. Just Show me the Demo!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 1 directory.. Python >= 3.8.x must be used as specified in the requirements You can test connecting to your camera through BLE using the following script: python ble_connect.py See the help for parameter definitions: $ python ble_connect.py --help usage: ble_connect.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, pair, then enable notifications. 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 1” from the dropdown and click on “Perform.” Perform Tutorial 1 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. Basic BLE Tutorial This tutorial will walk through the process of connecting to a GoPro via BLE. This same connect functionality will be used as a foundation for all future BLE tutorials. Here is a summary of the sequence that will be described in detail in the following sections: Open GoPro user deviceGoProScanningConnectedalt[If not Previously Paired]PairedReady to CommunicateAdvertisingAdvertisingConnectPair RequestPair ResponseEnable Notifications on Characteristic 1Enable Notifications on Characteristic 2Enable Notifications on Characteristic ..Enable Notifications on Characteristic NOpen GoPro user deviceGoPro Advertise First, we need to ensure the camera is discoverable (i.e. it is advertising). Follow the per-camera steps here. The screen should appear as such: Camera is discoverable. Scan Next, we must scan to discover the advertising GoPro Camera. python kotlin We will do this using bleak. Let’s initialize an empty dict that will store discovered devices, indexed by name: Map of devices indexed by name devices: Dict[str, BleakDevice] = {} We’re then going to scan for all devices. We are passing a scan callback to bleak in order to also find non-connectable scan responses. We are keeping any devices that have a device name. Scan callback to also catch nonconnectable scan responses def _scan_callback(device: BleakDevice, _: Any) -> None: Add to the dict if not unknown if device.name != \"Unknown\" and device.name is not None: devices[device.name] = device Now discover and add connectable advertisements for device in await BleakScanner.discover(timeout=5, detection_callback=_scan_callback): if device.name != \"Unknown\" and device.name is not None: devices[device.name] = device Now we can search through the discovered devices to see if we found a GoPro. Any GoPro device name will be structured as GoPro XXXX where XXXX is the last four digits of your camera’s serial number. If you have renamed your GoPro to something other than the default, you will need to update the below steps accordingly. First, we define a regex which is either “GoPro “ followed by any four alphanumeric characters if no identifier was passed, or “GoPro “ concatenated with the identifier if it exists. In the demo ble_connect.py, the identifier is taken from the command-line arguments. token = re.compile(r\"GoPro [A-Z0-9]{4}\" if identifier is None else f\"GoPro {identifier}\") Now we build a list of matched devices by checking if each device’s name includes the token regex. matched_devices: List[BleakDevice] = [] Now look for our matching device(s) matched_devices = [device for name, device in devices.items() if token.match(name)] Due to potential RF interference and the asynchronous nature of BLE advertising / scanning, it is possible that the advertising GoPro will not be discovered by the scanning PC in one scan. Therefore, you may need to redo the scan (as ble_connect.py does) until a GoPro is found. That is, matched_device must contain at least one device. Similarly, connection establishment can fail for reasons out of our control. Therefore, the connection process is also wrapped in retry logic. Here is an example of the log from ble_connect.py of scanning for devices. Note that this includes several rescans until the devices was found. $ python ble_connect.py INFO:root:Scanning for bluetooth devices... INFO:root: Discovered: INFO:root: Discovered: TR8600 seri INFO:root:Found 0 matching devices. INFO:root: Discovered: INFO:root: Discovered: TR8600 seri INFO:root: Discovered: GoPro Cam INFO:root: Discovered: GoPro 0456 INFO:root:Found 1 matching devices. Among other devices, you should see GoPro XXXX where XXXX is the last four digits of your camera’s serial number. First let’s define a filter to find the GoPro. We do this by filtering on the GoPro Service UUID that is included in all GoPro advertisements: private val scanFilters = listOf<ScanFilter>( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(GOPRO_UUID)).build() ) We then send this to the BLE API and collect events from the SharedFlow that it returns. We take the first event emitted from this SharedFlow and notify (via a Channel) that a GoPro advertiser has been found, store the GoPro’s BLE address, and stop the scan. ble.startScan(scanFilters).onSuccess { scanResults -> val deviceChannel: Channel<BluetoothDevice> = Channel() // Collect scan results CoroutineScope(Dispatchers.IO).launch { scanResults.collect { scanResult -> // We will take the first discovered gopro deviceChannel.send(scanResult.device) } } // Wait to receive the scan result goproAddress = deviceChannel.receive().address ble.stopScan(scanResults) } At this point, the GoPro’s BLE address is stored (as a String) in goproAddress. Here is an example log output from this process: Scanning for GoPro's Received scan result: GoPro 0992 Found GoPro: GoPro 0992 Connect Now that we have discovered at least one GoPro device to connect to, the next step is to establish a BLE connection to the camera. python kotlin We're just taking the first device if there are multiple. device = matched_devices[0] client = BleakClient(device) await client.connect(timeout=15) An example output of this is shown here where we can see that the connection has successfully been established as well as the GoPro’s BLE MAC address.: INFO:root:Establishing BLE connection to EF:5A:F6:13:E6:5A: GoPro 0456... INFO:bleak.backends.dotnet.client:Services resolved for BleakClientDotNet (EF:5A:F6:13:E6:5A) INFO:root:BLE Connected! ble.connect(goproAddress) At this point, the BLE connection is established but there is more setup to be done before we are ready to communicate. Pair The GoPro has encryption-protected characteristics which require us to pair before writing to them. Therefore now that we are connected, we need to attempt to pair. python kotlin try: await client.pair() except NotImplementedError: This is expected on Mac pass Not all OS’s allow pairing (at this time) but some require it. Rather than checking for the OS, we are just catching the exception when it fails. Rather than explicitly request pairing, we rely on the fact that Android will automatically start the pairing process if you try to read a characteristic that requires encryption. To do this, we read the Wifi AP Password characteristic. First we discover all characteristics (this will also be needed later when enabling notifications): ble.discoverCharacteristics(goproAddress) Then we read the relevant characteristic to trigger pairing: ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_PASSWORD.uuid) At this point a pairing popup should occur on the Android Device. Select “Allow Pairing” to continue. Here is an example log output from this process: Discovering characteristics Discovered 9 services for F7:5B:5D:81:64:1B Service 00001801-0000-1000-8000-00805f9b34fb Characteristics: |-- Service 00001800-0000-1000-8000-00805f9b34fb Characteristics: |--00002a00-0000-1000-8000-00805f9b34fb: READABLE |--00002a01-0000-1000-8000-00805f9b34fb: READABLE |--00002a04-0000-1000-8000-00805f9b34fb: READABLE Service 0000180f-0000-1000-8000-00805f9b34fb Characteristics: |--00002a19-0000-1000-8000-00805f9b34fb: READABLE, NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service 0000180a-0000-1000-8000-00805f9b34fb Characteristics: |--00002a29-0000-1000-8000-00805f9b34fb: READABLE |--00002a24-0000-1000-8000-00805f9b34fb: READABLE |--00002a25-0000-1000-8000-00805f9b34fb: READABLE |--00002a27-0000-1000-8000-00805f9b34fb: READABLE |--00002a26-0000-1000-8000-00805f9b34fb: READABLE |--00002a28-0000-1000-8000-00805f9b34fb: READABLE |--00002a23-0000-1000-8000-00805f9b34fb: READABLE |--00002a50-0000-1000-8000-00805f9b34fb: READABLE Service b5f90001-aa8d-11e3-9046-0002a5d5c51b Characteristics: |--b5f90002-aa8d-11e3-9046-0002a5d5c51b: READABLE, WRITABLE |--b5f90003-aa8d-11e3-9046-0002a5d5c51b: READABLE, WRITABLE |--b5f90004-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90005-aa8d-11e3-9046-0002a5d5c51b: READABLE, INDICATABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90006-aa8d-11e3-9046-0002a5d5c51b: READABLE Service 0000fea6-0000-1000-8000-00805f9b34fb Characteristics: |--b5f90072-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90073-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90074-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90075-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90076-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90077-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90078-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90079-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service b5f90090-aa8d-11e3-9046-0002a5d5c51b Characteristics: |--b5f90091-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90092-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service b5f90080-aa8d-11e3-9046-0002a5d5c51b Characteristics: |--b5f90081-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90082-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90083-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90084-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service 00001804-0000-1000-8000-00805f9b34fb Characteristics: |--00002a07-0000-1000-8000-00805f9b34fb: READABLE Pairing Read characteristic b5f90003-aa8d-11e3-9046-0002a5d5c51b : value: 66:3F:54:2D:38:35:72:2D:4E:35:63 Once paired, the camera should beep and display “Connection Successful”. This pairing process only needs to be done once. On subsequent connections, the devices will automatically re-establish encryption using stored keys. That is, they are “bonded.” Enable Notifications As specified in the Open GoPro Bluetooth API, we must enable notifications for a given characteristic to receive responses from it. To enable notifications, we loop over each characteristic in each service and enable the characteristic for notification if it has notify properties: python kotlin It is necessary to define a notification handler to pass to the bleak start_notify method. Since we only care about connecting to the device in this tutorial (and not actually receiving data), we are just passing an empty function. A future tutorial will demonstrate how to use this meaningfully. for service in client.services: for char in service.characteristics: if \"notify\" in char.properties: await client.start_notify(char, notification_handler) In the following example output, we can see that notifications are enabled for each characteristic that is notifiable. INFO:root:Enabling notifications... INFO:root:Enabling notification on char 00002a19-0000-1000-8000-00805f9b34fb INFO:root:Enabling notification on char b5f90073-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90075-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90077-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90079-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90092-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90081-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90083-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90084-aa8d-11e3-9046-0002a5d5c51b INFO:root:Done enabling notifications ble.servicesOf(goproAddress).onSuccess { services -> services.forEach { service -> service.characteristics.forEach { char -> if (char.isNotifiable()) { ble.enableNotification(goproAddress, char.uuid) } } } } Here is an example log output from this process: Enabling notifications Enabling notifications for 00002a19-0000-1000-8000-00805f9b34fb Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90073-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90075-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90077-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90079-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90092-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90081-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90083-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90084-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Bluetooth is ready for communication! The characteristics that correspond to each UUID listed in the log can be found in the Open GoPro API. These will be used in a future tutorial to send data. Once the notifications are enabled, the GoPro BLE initialization is complete and it is ready to communicate via BLE. Quiz time! 📚 ✏️ How often is it necessary to pair? A: Pairing must occur every time to ensure safe BLE communication. B: We never need to pair as the GoPro does not require it to communicate. C: Pairing only needs to occur once as the keys will be automatically re-used for future connections. Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. Pairing is only needed once (assuming neither side deletes the keys). If the GoPro deletes the keys (via Connections->Reset Connections), the devices will need to re-pair. Troubleshooting Device not connecting If the connection is not starting, it is likely because the camera is not advertising. This can be due to either: The camera is not in pairing mode. Ensure that this is achieved as done in the advertise section. The devices never disconnected from the previous session so are thus already connected. If this is the case, perform the “Complete System Reset” shown below. Complete System Reset BLE is a fickle beast. If at any point it is impossible to discover or connect to the camera, perform the following. Reset the camera by choosing Connections –> Reset Connections Use your OS’s bluetooth settings GUI to remove / unpair the Gopro Restart the procedure detailed above Logs python kotlin The demo program has enabled bleak logs and is also using the default python logging module to write its own logs. To enable more bleak logs, follow bleak’s troubleshooting section. The demo program is using Timber. It is piping all log messages to the UI but they are also available in the logcat window and can be filtered using: package:mine tag:GP_. Good Job! Congratulations 🤙 You can now successfully connect to the GoPro via BLE and prepare it to receive / send data. To see how to send commands, you should advance to the next tutorial.", + "excerpt": "This tutorial will provide a walk-through to connect to the GoPro camera via Bluetooth Low Energy (BLE). Requirements Hardware A GoPro camera that is supported by Open GoPro python kotlin One of the following systems: Windows 10, version 16299 (Fall Creators Update) or greater Linux distribution with BlueZ >= 5.43 OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 An Android Device supporting SDK >= 33 Software python kotlin Python >= 3.8.x must be installed. See this Python installation guide. Android Studio >= 2022.1.1 (Electric Eel) Overview / Assumptions python kotlin This tutorial will use bleak to control the OS’s Bluetooth Low Energy (BLE). The Bleak BLE controller does not currently support autonomous pairing for the BlueZ backend. So if you are using BlueZ (i.e. Ubuntu, RaspberryPi, etc.), you need to first pair the camera from the command line as shown in the BlueZ tutorial. There is work to add this feature and progress can be tracked on the Github Issue. The bleak module is based on asyncio which means that its awaitable functions need to be called from an async coroutine. In order to do this, all of the code below should be running in an async function. We accomplish this in the tutorial scripts by making main async as such: import asyncio async def main() -> None: Put our code here if __name__ == \"__main__\": asyncio.run(main()) These are stripped down Python tutorials that are only meant to show the basics. For a complete Python SDK that uses bleak as the backend as well as a cross-platform WiFi backend to easily write Python apps that control the GoPro, see the Open GoPro Python SDK This tutorial will provide a set of Kotlin tutorials to demonstrate Open GoPro Functionality. The tutorials are provided as a single Android Studio project targeted to run on an Android device. The tutorials are only concerned with application-level demonstrations of the Open GoPro API and therefore do not prioritize the following: UI: The tutorial project only contains a minimal UI to select, implement, and view logs for each tutorial Android architecture / best practices: the project architecture is designed to encapsulate Kotlin functionality to easily display per-tutorial functionality Android-specific requirements: permission handling, adapter enabling, etc are implemented in the project but not documented in the tutorials BLE / Wifi (HTTP) functionality: A simple BLE API is included in the project and will be touched upon in the tutorials. However, the focus of the tutorials is not on how the BLE API is implemented as a real project would likely use a third-party library for this such as Kable See the Punchthrough tutorials for Android BLE-Specific tutorials These tutorials assume familiarity and a base level of competence with: Android Studio Bluetooth Low Energy JSON HTTP Setup python kotlin This set of tutorials is accompanied by a Python package consisting of scripts separated by tutorial module. These can be found on Github. Once the Github repo has been cloned or downloaded to your local machine, the package can be installed as follows: Enter the python tutorials directory at $INSTALL/demos/python/tutorial/ where $INSTALL is the top level of the Open GoPro repo where it exists on your local machine Use pip to install the package (in editable mode in case you want to test out some changes): pip install -e . While it is out of the scope of this tutorial to describe, it is recommended to install the package in to a virtual environment in order to isolate system dependencies. You can test that installation was successful by viewing the installed package’s information: $ pip show open-gopro-python-tutorials Name: open-gopro-python-tutorials Version: 0.0.3 Summary: Open GoPro Python Tutorials Home-page: https://github.com/gopro/OpenGoPro Author: Tim Camise Author-email: gopro.com License: MIT Location: c:\\users\\tim\\gopro\\opengopro\\demos\\python\\tutorial Requires: bleak, requests Required-by: This set of tutorials is accompanied by an Android Studio project consisting of, among other project infrastructure, Kotlin files separated by tutorial module. The project can be found on Github. Once the Github repo has been cloned or downloaded to your local machine, open the project in Android studio. At this point you should be able to build and load the project to your Android device. The project will not work on an emulated device since BLE can not be emulated. Just Show me the Demo!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 1 directory.. Python >= 3.8.x must be used as specified in the requirements You can test connecting to your camera through BLE using the following script: python ble_connect.py See the help for parameter definitions: $ python ble_connect.py --help usage: ble_connect.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, pair, then enable notifications. 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 1” from the dropdown and click on “Perform.” Perform Tutorial 1 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. Basic BLE Tutorial This tutorial will walk through the process of connecting to a GoPro via BLE. This same connect functionality will be used as a foundation for all future BLE tutorials. Here is a summary of the sequence that will be described in detail in the following sections: GoProOpen GoPro user deviceGoProOpen GoPro user deviceScanningConnectedalt[If not Previously Paired]PairedReady to CommunicateAdvertisingAdvertisingConnectPair RequestPair ResponseEnable Notifications on Characteristic 1Enable Notifications on Characteristic 2Enable Notifications on Characteristic ..Enable Notifications on Characteristic N Advertise First, we need to ensure the camera is discoverable (i.e. it is advertising). Follow the per-camera steps here. The screen should appear as such: Camera is discoverable. Scan Next, we must scan to discover the advertising GoPro Camera. python kotlin We will do this using bleak. Let’s initialize an empty dict that will store discovered devices, indexed by name: Map of devices indexed by name devices: Dict[str, BleakDevice] = {} We’re then going to scan for all devices. We are passing a scan callback to bleak in order to also find non-connectable scan responses. We are keeping any devices that have a device name. Scan callback to also catch nonconnectable scan responses def _scan_callback(device: BleakDevice, _: Any) -> None: Add to the dict if not unknown if device.name != \"Unknown\" and device.name is not None: devices[device.name] = device Now discover and add connectable advertisements for device in await BleakScanner.discover(timeout=5, detection_callback=_scan_callback): if device.name != \"Unknown\" and device.name is not None: devices[device.name] = device Now we can search through the discovered devices to see if we found a GoPro. Any GoPro device name will be structured as GoPro XXXX where XXXX is the last four digits of your camera’s serial number. If you have renamed your GoPro to something other than the default, you will need to update the below steps accordingly. First, we define a regex which is either “GoPro “ followed by any four alphanumeric characters if no identifier was passed, or “GoPro “ concatenated with the identifier if it exists. In the demo ble_connect.py, the identifier is taken from the command-line arguments. token = re.compile(r\"GoPro [A-Z0-9]{4}\" if identifier is None else f\"GoPro {identifier}\") Now we build a list of matched devices by checking if each device’s name includes the token regex. matched_devices: List[BleakDevice] = [] Now look for our matching device(s) matched_devices = [device for name, device in devices.items() if token.match(name)] Due to potential RF interference and the asynchronous nature of BLE advertising / scanning, it is possible that the advertising GoPro will not be discovered by the scanning PC in one scan. Therefore, you may need to redo the scan (as ble_connect.py does) until a GoPro is found. That is, matched_device must contain at least one device. Similarly, connection establishment can fail for reasons out of our control. Therefore, the connection process is also wrapped in retry logic. Here is an example of the log from ble_connect.py of scanning for devices. Note that this includes several rescans until the devices was found. $ python ble_connect.py INFO:root:Scanning for bluetooth devices... INFO:root: Discovered: INFO:root: Discovered: TR8600 seri INFO:root:Found 0 matching devices. INFO:root: Discovered: INFO:root: Discovered: TR8600 seri INFO:root: Discovered: GoPro Cam INFO:root: Discovered: GoPro 0456 INFO:root:Found 1 matching devices. Among other devices, you should see GoPro XXXX where XXXX is the last four digits of your camera’s serial number. First let’s define a filter to find the GoPro. We do this by filtering on the GoPro Service UUID that is included in all GoPro advertisements: private val scanFilters = listOf<ScanFilter>( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(GOPRO_UUID)).build() ) We then send this to the BLE API and collect events from the SharedFlow that it returns. We take the first event emitted from this SharedFlow and notify (via a Channel) that a GoPro advertiser has been found, store the GoPro’s BLE address, and stop the scan. ble.startScan(scanFilters).onSuccess { scanResults -> val deviceChannel: Channel<BluetoothDevice> = Channel() // Collect scan results CoroutineScope(Dispatchers.IO).launch { scanResults.collect { scanResult -> // We will take the first discovered gopro deviceChannel.send(scanResult.device) } } // Wait to receive the scan result goproAddress = deviceChannel.receive().address ble.stopScan(scanResults) } At this point, the GoPro’s BLE address is stored (as a String) in goproAddress. Here is an example log output from this process: Scanning for GoPro's Received scan result: GoPro 0992 Found GoPro: GoPro 0992 Connect Now that we have discovered at least one GoPro device to connect to, the next step is to establish a BLE connection to the camera. python kotlin We're just taking the first device if there are multiple. device = matched_devices[0] client = BleakClient(device) await client.connect(timeout=15) An example output of this is shown here where we can see that the connection has successfully been established as well as the GoPro’s BLE MAC address.: INFO:root:Establishing BLE connection to EF:5A:F6:13:E6:5A: GoPro 0456... INFO:bleak.backends.dotnet.client:Services resolved for BleakClientDotNet (EF:5A:F6:13:E6:5A) INFO:root:BLE Connected! ble.connect(goproAddress) At this point, the BLE connection is established but there is more setup to be done before we are ready to communicate. Pair The GoPro has encryption-protected characteristics which require us to pair before writing to them. Therefore now that we are connected, we need to attempt to pair. python kotlin try: await client.pair() except NotImplementedError: This is expected on Mac pass Not all OS’s allow pairing (at this time) but some require it. Rather than checking for the OS, we are just catching the exception when it fails. Rather than explicitly request pairing, we rely on the fact that Android will automatically start the pairing process if you try to read a characteristic that requires encryption. To do this, we read the Wifi AP Password characteristic. First we discover all characteristics (this will also be needed later when enabling notifications): ble.discoverCharacteristics(goproAddress) Then we read the relevant characteristic to trigger pairing: ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_PASSWORD.uuid) At this point a pairing popup should occur on the Android Device. Select “Allow Pairing” to continue. Here is an example log output from this process: Discovering characteristics Discovered 9 services for F7:5B:5D:81:64:1B Service 00001801-0000-1000-8000-00805f9b34fb Characteristics: |-- Service 00001800-0000-1000-8000-00805f9b34fb Characteristics: |--00002a00-0000-1000-8000-00805f9b34fb: READABLE |--00002a01-0000-1000-8000-00805f9b34fb: READABLE |--00002a04-0000-1000-8000-00805f9b34fb: READABLE Service 0000180f-0000-1000-8000-00805f9b34fb Characteristics: |--00002a19-0000-1000-8000-00805f9b34fb: READABLE, NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service 0000180a-0000-1000-8000-00805f9b34fb Characteristics: |--00002a29-0000-1000-8000-00805f9b34fb: READABLE |--00002a24-0000-1000-8000-00805f9b34fb: READABLE |--00002a25-0000-1000-8000-00805f9b34fb: READABLE |--00002a27-0000-1000-8000-00805f9b34fb: READABLE |--00002a26-0000-1000-8000-00805f9b34fb: READABLE |--00002a28-0000-1000-8000-00805f9b34fb: READABLE |--00002a23-0000-1000-8000-00805f9b34fb: READABLE |--00002a50-0000-1000-8000-00805f9b34fb: READABLE Service b5f90001-aa8d-11e3-9046-0002a5d5c51b Characteristics: |--b5f90002-aa8d-11e3-9046-0002a5d5c51b: READABLE, WRITABLE |--b5f90003-aa8d-11e3-9046-0002a5d5c51b: READABLE, WRITABLE |--b5f90004-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90005-aa8d-11e3-9046-0002a5d5c51b: READABLE, INDICATABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90006-aa8d-11e3-9046-0002a5d5c51b: READABLE Service 0000fea6-0000-1000-8000-00805f9b34fb Characteristics: |--b5f90072-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90073-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90074-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90075-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90076-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90077-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90078-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90079-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service b5f90090-aa8d-11e3-9046-0002a5d5c51b Characteristics: |--b5f90091-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90092-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service b5f90080-aa8d-11e3-9046-0002a5d5c51b Characteristics: |--b5f90081-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90082-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90083-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90084-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE |------00002902-0000-1000-8000-00805f9b34fb: EMPTY Service 00001804-0000-1000-8000-00805f9b34fb Characteristics: |--00002a07-0000-1000-8000-00805f9b34fb: READABLE Pairing Read characteristic b5f90003-aa8d-11e3-9046-0002a5d5c51b : value: 66:3F:54:2D:38:35:72:2D:4E:35:63 Once paired, the camera should beep and display “Connection Successful”. This pairing process only needs to be done once. On subsequent connections, the devices will automatically re-establish encryption using stored keys. That is, they are “bonded.” Enable Notifications As specified in the Open GoPro Bluetooth API, we must enable notifications for a given characteristic to receive responses from it. To enable notifications, we loop over each characteristic in each service and enable the characteristic for notification if it has notify properties: python kotlin It is necessary to define a notification handler to pass to the bleak start_notify method. Since we only care about connecting to the device in this tutorial (and not actually receiving data), we are just passing an empty function. A future tutorial will demonstrate how to use this meaningfully. for service in client.services: for char in service.characteristics: if \"notify\" in char.properties: await client.start_notify(char, notification_handler) In the following example output, we can see that notifications are enabled for each characteristic that is notifiable. INFO:root:Enabling notifications... INFO:root:Enabling notification on char 00002a19-0000-1000-8000-00805f9b34fb INFO:root:Enabling notification on char b5f90073-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90075-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90077-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90079-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90092-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90081-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90083-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90084-aa8d-11e3-9046-0002a5d5c51b INFO:root:Done enabling notifications ble.servicesOf(goproAddress).onSuccess { services -> services.forEach { service -> service.characteristics.forEach { char -> if (char.isNotifiable()) { ble.enableNotification(goproAddress, char.uuid) } } } } Here is an example log output from this process: Enabling notifications Enabling notifications for 00002a19-0000-1000-8000-00805f9b34fb Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90073-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90075-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90077-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90079-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90092-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90081-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90083-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Enabling notifications for b5f90084-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Bluetooth is ready for communication! The characteristics that correspond to each UUID listed in the log can be found in the Open GoPro API. These will be used in a future tutorial to send data. Once the notifications are enabled, the GoPro BLE initialization is complete and it is ready to communicate via BLE. Quiz time! 📚 ✏️ How often is it necessary to pair? A: Pairing must occur every time to ensure safe BLE communication. B: We never need to pair as the GoPro does not require it to communicate. C: Pairing only needs to occur once as the keys will be automatically re-used for future connections. Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. Pairing is only needed once (assuming neither side deletes the keys). If the GoPro deletes the keys (via Connections->Reset Connections), the devices will need to re-pair. Troubleshooting Device not connecting If the connection is not starting, it is likely because the camera is not advertising. This can be due to either: The camera is not in pairing mode. Ensure that this is achieved as done in the advertise section. The devices never disconnected from the previous session so are thus already connected. If this is the case, perform the “Complete System Reset” shown below. Complete System Reset BLE is a fickle beast. If at any point it is impossible to discover or connect to the camera, perform the following. Reset the camera by choosing Connections –> Reset Connections Use your OS’s bluetooth settings GUI to remove / unpair the Gopro Restart the procedure detailed above Logs python kotlin The demo program has enabled bleak logs and is also using the default python logging module to write its own logs. To enable more bleak logs, follow bleak’s troubleshooting section. The demo program is using Timber. It is piping all log messages to the UI but they are also available in the logcat window and can be filtered using: package:mine tag:GP_. Good Job! Congratulations 🤙 You can now successfully connect to the GoPro via BLE and prepare it to receive / send data. To see how to send commands, you should advance to the next tutorial.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/connect-ble#" }, { "title": "Tutorial 2: Send BLE Commands: ", - "excerpt": "This document will provide a walk-through tutorial to use the Open GoPro BLE Interface to send commands and receive responses. “Commands” in this sense are specifically procedures that are initiated by either: Writing to the Command Request UUID and receiving responses via the Command Response UUID. They are listed here. Writing to the Setting UUID and receiving responses via the Setting Response UUID. They are listed here. It is suggested that you have first completed the connect tutorial before going through this tutorial. This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future lab. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements Set Shutter You can test sending the Set Shutter command to your camera through BLE using the following script: $ python ble_command_set_shutter.py See the help for parameter definitions: $ python ble_command_set_shutter.py --help usage: ble_command_set_shutter.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, set the shutter on, wait 2 seconds, then set the shutter off. 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 Load Preset Group You can test sending the Load Preset Group command to your camera through BLE using the following script: $ python ble_command_load_group.py See the help for parameter definitions: $ python ble_command_load_group.py --help usage: ble_command_load_group.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, then change the Preset Group to Video. 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 Set the Video Resolution You can test sending the Set Video Resolution command to your camera through BLE using the following script: $ python ble_command_set_resolution.py See the help for parameter definitions: $ python ble_command_set_resolution.py --help usage: ble_command_set_resolution.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, then change the resolution to 1080. 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 Set the Frames Per Second (FPS) You can test sending the Set FPS command to your camera through BLE using the following script: $ python ble_command_set_fps.py See the help for parameter definitions: $ python ble_command_set_fps.py --help usage: ble_command_set_fps.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, then attempt to change the fps to 240. 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 2” 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 2 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect as was discussed in the connect tutorial. In this case, however, we are defining a meaningful (albeit naive) notification handler that will: print byte data and handle that the notification was received on check if the response is what we expected set an event to notify the writer that the response was received This is a very simple handler; response parsing will be expanded upon in the next tutorial. python kotlin def notification_handler(handle: int, data: bytes) -> None: logger.info(f'Received response at {handle=}: {hexlify(data, \":\")!r}') If this is the correct handle and the status is success, the command was a success if client.services.characteristics[handle].uuid == response_uuid and data[2] == 0x00: logger.info(\"Command sent successfully\") Anything else is unexpected. This shouldn't happen else: logger.error(\"Unexpected response\") Notify the writer event.set() The event used above is a simple synchronization event that is only alerting the writer that a notification was received. For now, we’re just checking that the handle matches what is expected and that the status (third byte) is success (0x00). private val receivedData: Channel<UByteArray> = Channel() private fun naiveNotificationHandler(characteristic: UUID, data: UByteArray) { if ((characteristic == GoProUUID.CQ_COMMAND_RSP.uuid)) { CoroutineScope(Dispatchers.IO).launch { receivedData.send(data) } } } private val bleListeners by lazy { BleEventListener().apply { onNotification = ::naiveNotificationHandler } } The handler is simply verifying that the response was received on the correct UIUD and then notifying the received data. We are registering this notification handler with the BLE API before sending any data requests as such: ble.registerListener(goproAddress, bleListeners) There is much more to the synchronization and data parsing than this but this will be discussed in future tutorials. Command Overview Both Command Requests and Setting Requests follow the same procedure: Write to relevant request UUID Receive confirmation from GoPro (via notification from relevant response UUID) that request was received. GoPro reacts to command The notification response only indicates that the request was received and whether it was accepted or rejected. The relevant behavior of the GoPro must be observed to verify when the command’s effects have been applied. Here is the procedure from power-on to finish: Open GoPro user deviceGoProdevices are connected as in Tutorial 1Command Request (Write to Request UUID)Command Response (via notification to Response UUID)Apply effects of command when ableOpen GoPro user deviceGoPro Sending Commands Now that we are are connected, paired, and have enabled notifications (registered to our defined callback), we can send some commands. First, we need to define the attributes to write to / receive responses from, which are: For commands “Command Request” characteristic (UUID b5f90072-aa8d-11e3-9046-0002a5d5c51b) “Command Response” characteristic (UUID b5f90073-aa8d-11e3-9046-0002a5d5c51b) For settings “Settings” characteristic (UUID b5f90074-aa8d-11e3-9046-0002a5d5c51b) “Settings Response” (UUID b5f90075-aa8d-11e3-9046-0002a5d5c51b) python kotlin COMMAND_REQ_UUID = GOPRO_BASE_UUID.format(\"0072\") COMMAND_RSP_UUID = GOPRO_BASE_UUID.format(\"0073\") SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format(\"0074\") SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format(\"0075\") We’re using the GOPRO_BASE_UUID string imported from the module’s __init__.py to build these. These are defined in the GoProUUID class: const val GOPRO_UUID = \"0000FEA6-0000-1000-8000-00805f9b34fb\" const val GOPRO_BASE_UUID = \"b5f9%s-aa8d-11e3-9046-0002a5d5c51b\" enum class GoProUUID(val uuid: UUID) { WIFI_AP_PASSWORD(UUID.fromString(GOPRO_BASE_UUID.format(\"0003\"))), WIFI_AP_SSID(UUID.fromString(GOPRO_BASE_UUID.format(\"0002\"))), CQ_COMMAND(UUID.fromString(GOPRO_BASE_UUID.format(\"0072\"))), CQ_COMMAND_RSP(UUID.fromString(GOPRO_BASE_UUID.format(\"0073\"))), CQ_SETTING(UUID.fromString(GOPRO_BASE_UUID.format(\"0074\"))), CQ_SETTING_RSP(UUID.fromString(GOPRO_BASE_UUID.format(\"0075\"))), CQ_QUERY(UUID.fromString(GOPRO_BASE_UUID.format(\"0076\"))), CQ_QUERY_RSP(UUID.fromString(GOPRO_BASE_UUID.format(\"0077\"))); } Set Shutter The first command we will be sending is Set Shutter, which at byte level is: Command Bytes Set Shutter Off 0x03 0x01 0x01 0x00 Set Shutter On 0x03 0x01 0x01 0x01 Now, let’s write the bytes to the “Command Request” UUID to turn the shutter on and start encoding! python kotlin event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 1])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val setShutterOnCmd = ubyteArrayOf(0x03U, 0x01U, 0x01U, 0x01U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setShutterOnCmd) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should hear the camera beep and it will either take a picture or start recording depending on what mode it is in. Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications. This can be seen in the demo log: python kotlin INFO:root:Setting the shutter on INFO:root:Received response at handle=52: b'02:01:00' INFO:root:Shutter command sent successfully Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:01:01:01 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:01:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:01:00 Command sent successfully As expected, the response was received on the correct handle and the status was “success”. If you are recording a video, continue reading to set the shutter off. We can now set the shutter off: We’re waiting 2 seconds in case you are in video mode so that we can capture a 2 second video. python kotlin time.sleep(2) event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 0])) await event.wait() Wait to receive the notification response This will log in the console as follows: INFO:root:Setting the shutter off INFO:root:Received response at handle=52: b'02:01:00' INFO:root:Shutter command sent successfully delay(2000) val setShutterOffCmd = ubyteArrayOf(0x03U, 0x01U, 0x01U, 0x00U) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) This will log as such: Setting the shutter off Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:01:01:00 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:01:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:01:00 Command sent successfully Load Preset Group The next command we will be sending is Load Preset Group, which is used to toggle between the 3 groups of presets (video, photo, and timelapse). At byte level, the commands are: Command Bytes Load Video Preset Group 0x04 0x3E 0x02 0x03 0xE8 Load Photo Preset Group 0x04 0x3E 0x02 0x03 0xE9 Load Timelapse Preset Group 0x04 0x3E 0x02 0x03 0xEA It is possible that the preset GroupID values will vary in future cameras. The only absolutely correct way to know the preset ID is to read them from the “Get Preset Status” protobuf command. A future lab will discuss protobuf commands. Now, let’s write the bytes to the “Command Request” UUID to change the preset group to Video! python kotlin event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x04, 0x3E, 0x02, 0x03, 0xE8])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val loadPreset = ubyteArrayOf(0x04U, 0x3EU, 0x02U, 0x03U, 0xE8U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, loadPreset) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should hear the camera beep and move to the Video Preset Group. You can tell this by the logo at the top middle of the screen: Load Preset Group Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications. This can be seen in the demo log: python kotlin INFO:root:Loading the video preset group... INFO:root:Received response at handle=52: b'02:3e:00' INFO:root:Command sent successfully Loading Video Preset Group Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 04:3E:02:03:E8 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:3E:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:3E:00 Command status received Command sent successfully As expected, the response was received on the correct handle and the status was “success”. Set the Video Resolution The next command we will be sending is Set Video Resolution. This is used to change the value of the Video Resolution setting. It is important to note that this only affects video resolution (not photo). Therefore, the Video Preset Group must be active in order for it to succeed. This can be done either manually through the camera UI or by sending Load Preset Group. This resolution only affects the current video preset. Each video preset can have its own independent values for video resolution. Here are some of the byte level commands for various video resolutions. Command Bytes Set Video Resolution to 1080 0x03 0x02 0x01 0x09 Set Video Resolution to 2.7K 0x03 0x02 0x01 0x04 Set Video Resolution to 5K 0x03 0x02 0x01 0x18 Now, let’s write the bytes to the “Setting Request” UUID to change the video resolution to 1080! python kotlin event.clear() await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x02, 0x01, 0x09])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val setResolution = ubyteArrayOf(0x03U, 0x02U, 0x01U, 0x09U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setResolution) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should see the video resolution change to 1080 in the pill in the bottom-middle of the screen: Set Video Resolution Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications.. This can be seen in the demo log: python kotlin INFO:root:Loading the video preset group... INFO:root:Received response at handle=52: b'02:3e:00' INFO:root:Command sent successfully Setting resolution to 1080 Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:02:01:09 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:02:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:02:00 Command status received Command sent successfully As expected, the response was received on the correct handle and the status was “success”. If the Preset Group was not Video, the status will not be success. Set the Frames Per Second (FPS) The next command we will be sending is Set FPS. This is used to change the value of the FPS setting. It is important to note that this setting is dependent on the video resolution. That is, certain FPS values are not valid with certain resolutions. In general, higher resolutions only allow lower FPS values. Also, the current anti-flicker value may further limit possible FPS values. Check the camera capabilities to see which FPS values are valid for given use cases. Therefore, for this step of the tutorial, it is assumed that the resolution has been set to 1080 as in Set the Video Resolution. Here are some of the byte level commands for various FPS values. Command Bytes Set FPS to 24 0x03 0x03 0x01 0x0A Set FPS to 60 0x03 0x03 0x01 0x05 Set FPS to 240 0x03 0x03 0x01 0x00 Note that the possible FPS values can vary based on the Open GoPro version that the camera supports. Therefore, it is necessary to check the version. Now, let’s write the bytes to the “Setting Request” UUID to change the FPS to 240! python kotlin event.clear() await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x03, 0x01, 0x00])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val setFps = ubyteArrayOf(0x03U, 0x03U, 0x01U, 0x00U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setFps) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should see the FPS change to 240 in the pill in the bottom-middle of the screen: Set FPS Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications.. This can be seen in the demo log: python kotlin INFO:root:Setting the fps to 240 INFO:root:Received response at handle=57: b'02:03:00' INFO:root:Command sent successfully Setting the FPS to 240 Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:03:01:00 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:03:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:03:00 Command status received Command sent successfully As expected, the response was received on the correct handle and the status was “success”. If the video resolution was higher, for example 5K, this would fail. Quiz time! 📚 ✏️ Which of the following is not a real preset group? A: Timelapse B: Photo C: Burst D: Video Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There are 3 preset groups (Timelapse, Photo, and Video). These can be set via the Load Preset Group command. True or False: Every combination of resolution and FPS value is valid. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. Each resolution can support all or only some FPS values. You can find out which resolutions support which fps values by consulting the capabilities section of the spec. True or False: Every camera supports the same combination of resolution and FPS values. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. The only way to know what values are supported is to first check the Open GoPro version. See the relevant version of the BLE or WiFi spec to see what is supported. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now send any of the other BLE commands detailed in the Open GoPro documentation in a similar manner. To see how to parse more complicate responses, proceed to the next tutorial.", + "excerpt": "This document will provide a walk-through tutorial to use the Open GoPro BLE Interface to send commands and receive responses. “Commands” in this sense are specifically procedures that are initiated by either: Writing to the Command Request UUID and receiving responses via the Command Response UUID. They are listed here. Writing to the Setting UUID and receiving responses via the Setting Response UUID. They are listed here. It is suggested that you have first completed the connect tutorial before going through this tutorial. This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future lab. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements Set Shutter You can test sending the Set Shutter command to your camera through BLE using the following script: $ python ble_command_set_shutter.py See the help for parameter definitions: $ python ble_command_set_shutter.py --help usage: ble_command_set_shutter.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, set the shutter on, wait 2 seconds, then set the shutter off. 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 Load Preset Group You can test sending the Load Preset Group command to your camera through BLE using the following script: $ python ble_command_load_group.py See the help for parameter definitions: $ python ble_command_load_group.py --help usage: ble_command_load_group.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, then change the Preset Group to Video. 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 Set the Video Resolution You can test sending the Set Video Resolution command to your camera through BLE using the following script: $ python ble_command_set_resolution.py See the help for parameter definitions: $ python ble_command_set_resolution.py --help usage: ble_command_set_resolution.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, then change the resolution to 1080. 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 Set the Frames Per Second (FPS) You can test sending the Set FPS command to your camera through BLE using the following script: $ python ble_command_set_fps.py See the help for parameter definitions: $ python ble_command_set_fps.py --help usage: ble_command_set_fps.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, then attempt to change the fps to 240. 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 2” 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 2 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect as was discussed in the connect tutorial. In this case, however, we are defining a meaningful (albeit naive) notification handler that will: print byte data and handle that the notification was received on check if the response is what we expected set an event to notify the writer that the response was received This is a very simple handler; response parsing will be expanded upon in the next tutorial. python kotlin def notification_handler(handle: int, data: bytes) -> None: logger.info(f'Received response at {handle=}: {hexlify(data, \":\")!r}') If this is the correct handle and the status is success, the command was a success if client.services.characteristics[handle].uuid == response_uuid and data[2] == 0x00: logger.info(\"Command sent successfully\") Anything else is unexpected. This shouldn't happen else: logger.error(\"Unexpected response\") Notify the writer event.set() The event used above is a simple synchronization event that is only alerting the writer that a notification was received. For now, we’re just checking that the handle matches what is expected and that the status (third byte) is success (0x00). private val receivedData: Channel<UByteArray> = Channel() private fun naiveNotificationHandler(characteristic: UUID, data: UByteArray) { if ((characteristic == GoProUUID.CQ_COMMAND_RSP.uuid)) { CoroutineScope(Dispatchers.IO).launch { receivedData.send(data) } } } private val bleListeners by lazy { BleEventListener().apply { onNotification = ::naiveNotificationHandler } } The handler is simply verifying that the response was received on the correct UIUD and then notifying the received data. We are registering this notification handler with the BLE API before sending any data requests as such: ble.registerListener(goproAddress, bleListeners) There is much more to the synchronization and data parsing than this but this will be discussed in future tutorials. Command Overview Both Command Requests and Setting Requests follow the same procedure: Write to relevant request UUID Receive confirmation from GoPro (via notification from relevant response UUID) that request was received. GoPro reacts to command The notification response only indicates that the request was received and whether it was accepted or rejected. The relevant behavior of the GoPro must be observed to verify when the command’s effects have been applied. Here is the procedure from power-on to finish: GoProOpen GoPro user deviceGoProOpen GoPro user devicedevices are connected as in Tutorial 1Command Request (Write to Request UUID)Command Response (via notification to Response UUID)Apply effects of command when able Sending Commands Now that we are are connected, paired, and have enabled notifications (registered to our defined callback), we can send some commands. First, we need to define the attributes to write to / receive responses from, which are: For commands “Command Request” characteristic (UUID b5f90072-aa8d-11e3-9046-0002a5d5c51b) “Command Response” characteristic (UUID b5f90073-aa8d-11e3-9046-0002a5d5c51b) For settings “Settings” characteristic (UUID b5f90074-aa8d-11e3-9046-0002a5d5c51b) “Settings Response” (UUID b5f90075-aa8d-11e3-9046-0002a5d5c51b) python kotlin COMMAND_REQ_UUID = GOPRO_BASE_UUID.format(\"0072\") COMMAND_RSP_UUID = GOPRO_BASE_UUID.format(\"0073\") SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format(\"0074\") SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format(\"0075\") We’re using the GOPRO_BASE_UUID string imported from the module’s __init__.py to build these. These are defined in the GoProUUID class: const val GOPRO_UUID = \"0000FEA6-0000-1000-8000-00805f9b34fb\" const val GOPRO_BASE_UUID = \"b5f9%s-aa8d-11e3-9046-0002a5d5c51b\" enum class GoProUUID(val uuid: UUID) { WIFI_AP_PASSWORD(UUID.fromString(GOPRO_BASE_UUID.format(\"0003\"))), WIFI_AP_SSID(UUID.fromString(GOPRO_BASE_UUID.format(\"0002\"))), CQ_COMMAND(UUID.fromString(GOPRO_BASE_UUID.format(\"0072\"))), CQ_COMMAND_RSP(UUID.fromString(GOPRO_BASE_UUID.format(\"0073\"))), CQ_SETTING(UUID.fromString(GOPRO_BASE_UUID.format(\"0074\"))), CQ_SETTING_RSP(UUID.fromString(GOPRO_BASE_UUID.format(\"0075\"))), CQ_QUERY(UUID.fromString(GOPRO_BASE_UUID.format(\"0076\"))), CQ_QUERY_RSP(UUID.fromString(GOPRO_BASE_UUID.format(\"0077\"))); } Set Shutter The first command we will be sending is Set Shutter, which at byte level is: Command Bytes Set Shutter Off 0x03 0x01 0x01 0x00 Set Shutter On 0x03 0x01 0x01 0x01 Now, let’s write the bytes to the “Command Request” UUID to turn the shutter on and start encoding! python kotlin event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 1])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val setShutterOnCmd = ubyteArrayOf(0x03U, 0x01U, 0x01U, 0x01U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setShutterOnCmd) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should hear the camera beep and it will either take a picture or start recording depending on what mode it is in. Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications. This can be seen in the demo log: python kotlin INFO:root:Setting the shutter on INFO:root:Received response at handle=52: b'02:01:00' INFO:root:Shutter command sent successfully Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:01:01:01 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:01:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:01:00 Command sent successfully As expected, the response was received on the correct handle and the status was “success”. If you are recording a video, continue reading to set the shutter off. We can now set the shutter off: We’re waiting 2 seconds in case you are in video mode so that we can capture a 2 second video. python kotlin time.sleep(2) event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 0])) await event.wait() Wait to receive the notification response This will log in the console as follows: INFO:root:Setting the shutter off INFO:root:Received response at handle=52: b'02:01:00' INFO:root:Shutter command sent successfully delay(2000) val setShutterOffCmd = ubyteArrayOf(0x03U, 0x01U, 0x01U, 0x00U) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) This will log as such: Setting the shutter off Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:01:01:00 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:01:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:01:00 Command sent successfully Load Preset Group The next command we will be sending is Load Preset Group, which is used to toggle between the 3 groups of presets (video, photo, and timelapse). At byte level, the commands are: Command Bytes Load Video Preset Group 0x04 0x3E 0x02 0x03 0xE8 Load Photo Preset Group 0x04 0x3E 0x02 0x03 0xE9 Load Timelapse Preset Group 0x04 0x3E 0x02 0x03 0xEA It is possible that the preset GroupID values will vary in future cameras. The only absolutely correct way to know the preset ID is to read them from the “Get Preset Status” protobuf command. A future lab will discuss protobuf commands. Now, let’s write the bytes to the “Command Request” UUID to change the preset group to Video! python kotlin event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x04, 0x3E, 0x02, 0x03, 0xE8])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val loadPreset = ubyteArrayOf(0x04U, 0x3EU, 0x02U, 0x03U, 0xE8U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, loadPreset) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should hear the camera beep and move to the Video Preset Group. You can tell this by the logo at the top middle of the screen: Load Preset Group Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications. This can be seen in the demo log: python kotlin INFO:root:Loading the video preset group... INFO:root:Received response at handle=52: b'02:3e:00' INFO:root:Command sent successfully Loading Video Preset Group Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 04:3E:02:03:E8 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:3E:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:3E:00 Command status received Command sent successfully As expected, the response was received on the correct handle and the status was “success”. Set the Video Resolution The next command we will be sending is Set Video Resolution. This is used to change the value of the Video Resolution setting. It is important to note that this only affects video resolution (not photo). Therefore, the Video Preset Group must be active in order for it to succeed. This can be done either manually through the camera UI or by sending Load Preset Group. This resolution only affects the current video preset. Each video preset can have its own independent values for video resolution. Here are some of the byte level commands for various video resolutions. Command Bytes Set Video Resolution to 1080 0x03 0x02 0x01 0x09 Set Video Resolution to 2.7K 0x03 0x02 0x01 0x04 Set Video Resolution to 5K 0x03 0x02 0x01 0x18 Now, let’s write the bytes to the “Setting Request” UUID to change the video resolution to 1080! python kotlin event.clear() await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x02, 0x01, 0x09])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val setResolution = ubyteArrayOf(0x03U, 0x02U, 0x01U, 0x09U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setResolution) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should see the video resolution change to 1080 in the pill in the bottom-middle of the screen: Set Video Resolution Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications.. This can be seen in the demo log: python kotlin INFO:root:Loading the video preset group... INFO:root:Received response at handle=52: b'02:3e:00' INFO:root:Command sent successfully Setting resolution to 1080 Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:02:01:09 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:02:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:02:00 Command status received Command sent successfully As expected, the response was received on the correct handle and the status was “success”. If the Preset Group was not Video, the status will not be success. Set the Frames Per Second (FPS) The next command we will be sending is Set FPS. This is used to change the value of the FPS setting. It is important to note that this setting is dependent on the video resolution. That is, certain FPS values are not valid with certain resolutions. In general, higher resolutions only allow lower FPS values. Also, the current anti-flicker value may further limit possible FPS values. Check the camera capabilities to see which FPS values are valid for given use cases. Therefore, for this step of the tutorial, it is assumed that the resolution has been set to 1080 as in Set the Video Resolution. Here are some of the byte level commands for various FPS values. Command Bytes Set FPS to 24 0x03 0x03 0x01 0x0A Set FPS to 60 0x03 0x03 0x01 0x05 Set FPS to 240 0x03 0x03 0x01 0x00 Note that the possible FPS values can vary based on the Open GoPro version that the camera supports. Therefore, it is necessary to check the version. Now, let’s write the bytes to the “Setting Request” UUID to change the FPS to 240! python kotlin event.clear() await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x03, 0x01, 0x00])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val setFps = ubyteArrayOf(0x03U, 0x03U, 0x01U, 0x00U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setFps) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) You should see the FPS change to 240 in the pill in the bottom-middle of the screen: Set FPS Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled its notifications in Enable Notifications.. This can be seen in the demo log: python kotlin INFO:root:Setting the fps to 240 INFO:root:Received response at handle=57: b'02:03:00' INFO:root:Command sent successfully Setting the FPS to 240 Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:03:01:00 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:03:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:03:00 Command status received Command sent successfully As expected, the response was received on the correct handle and the status was “success”. If the video resolution was higher, for example 5K, this would fail. Quiz time! 📚 ✏️ Which of the following is not a real preset group? A: Timelapse B: Photo C: Burst D: Video Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There are 3 preset groups (Timelapse, Photo, and Video). These can be set via the Load Preset Group command. True or False: Every combination of resolution and FPS value is valid. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. Each resolution can support all or only some FPS values. You can find out which resolutions support which fps values by consulting the capabilities section of the spec. True or False: Every camera supports the same combination of resolution and FPS values. A: True B: False Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. The only way to know what values are supported is to first check the Open GoPro version. See the relevant version of the BLE or WiFi spec to see what is supported. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now send any of the other BLE commands detailed in the Open GoPro documentation in a similar manner. To see how to parse more complicate responses, proceed to the next tutorial.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/send-ble-commands#" }, { "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 commands instead return protobuf responses. These are not considered here and will be discussed in a future tutorial. It is suggested that you have first completed the connect and sending commands tutorials before going through this tutorial. This tutorial will give an overview of types of responses, then give examples of parsing each type before finally providing a Response class that will be used in future tutorials. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x 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_state.py See the help for parameter definitions: $ python ble_command_get_state.py --help usage: ble_command_get_state.py [-h] [-i IDENTIFIER] Connect to a GoPro camera via BLE, then get its statuses and settings. 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 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 connect 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 sending specific commands where this works and we know that the sequence always appears as such (connection sequence left out for brevity): Open GoPro user deviceGoProdevices are connected as in Tutorial 1Write to characteristicNotification Response (MSB == 0 (start))Open GoPro user deviceGoPro In actuality, responses can be more complicated. As described in the Open GoPro Interface, responses can be be comprised of multiple packets where each packet is <= 20 bytes such as: Open GoPro user deviceGoProdevices 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))Open GoPro user deviceGoPro This requires the implementation of accumulating and parsing algorithms which will be described in [Parsing Multiple Packet TLV Responses]. Parsing a One Packet TLV Response This section will describe how to parse one packet (<= 20 byte) responses. A one-packet response is formatted as such: Header (length) Command / Setting ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes Command / Setting Responses with Response 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 / Setting / Status 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. Complex Command Response There are some commands that do return additional response data. These are called “complex responses.” From the commands reference, we can see that these are: Get Open GoPro Version (ID == 0x51) Get Hardware Info (ID == 0x3C) In this tutorial, we will walk through creating a simple parser to parse the Open GoPro Get Version Command. 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 command to the Command Request UUID: python kotlin COMMAND_REQ_UUID = GOPRO_BASE_UUID.format(\"0072\") event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x01, 0x51])) await event.wait() Wait to receive the notification response We then receive a response at the expected handle. This is logged as: INFO:root:Getting the Open GoPro version... INFO:root:Received response at handle=52: b'06:51:00:01:02:01:00' val getVersion = ubyteArrayOf(0x01U, 0x51U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, getVersion) val version = receivedResponse.receive() as Response.Complex // Wait to receive response This is loged 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 b5f90073-aa8d-11e3-9046-0002a5d5c51b: 06:51:00:01:02:01:00 This response equates to: Header (length) Command / Setting / Status ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes 0x06 0x51 == Get Version 0x00 == Success 0x01 0x02 0x01 0x00 We can see that this “complex response” contains 4 additional bytes that need to be parsed. Using the information from the interface description, we know to parse this as: Byte Meaning 0x01 Length of Major Version Number 0x02 Major Version Number 0x01 Length of Minor Version Number 0x00 Minor Version Number We implement this in the notification handler as follows. First, we parse the length, command ID, and status from the first 3 bytes of the response. Then we parse the remaining four bytes of the response as individual values formatted as such: Length Value 1 byte Length bytes python kotlin The snippets of code included in this section are taken from the notification handler Parse first 3 bytes len = data[0] command_id = data[1] status = data[2] Parse remaining four bytes index = 3 params = [] while index <= len: param_len = data[index] index += 1 params.append(data[index : index + param_len]) index += param_len The snippets of code included in this section are taken from the Response.Complex parse method. For the contrived code in this tutorial, we have separate Response sealed classes to handle each use case. // Parse header bytes id = packet[0].toInt() status = packet[1].toInt() var buf = packet.drop(2) // Parse remaining packet while (buf.isNotEmpty()) { // Get each parameter's ID and length val paramLen = buf[0].toInt() buf = buf.drop(1) // Get the parameter's value val paramVal = buf.take(paramLen) // Store in data list data += paramVal.toUByteArray() // Advance the buffer for continued parsing buf = buf.drop(paramLen) } From the complex 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, minor = params logger.info(f\"Received a response to {command_id=} with {status=}: version={major[0]}.{minor[0]}\") which shows on the log as: INFO:root:Received a response to command_id=81 with status=0: version=2.0 val version = receivedResponse.receive() as Response.Complex // Wait to receive response val major = version.data[0].first().toInt() val minor = version.data[1].first().toInt() Timber.i(\"Got the Open GoPro version successfully: $major.$minor\") which shows on the log as such: Got the Open GoPro version successfully: 2.0 Quiz time! 📚 ✏️ What is the maximum size of an individual notification response packet? 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 packets 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. What is the maximum amount of packets that one response can be composed of? A: Always 1 packet B: Always multiple packets. C: Always 1 packet except for complex responses. Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. Command responses are almost always 1 packet (just returning the status). The exception are complex responses which can be multiple packets (in the case of Get Hardware Info) How many packets are setting responses comprised of? A: Always 1 packet B: Always multiple packets. C: Always 1 packet except for complex responses. Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. Settings Responses only ever contain the command status. Furthermore, there is no concept of complex responses for setting commands. Parsing Multiple Packet TLV Responses This section will describe parsing TLV responses that contain more than one packet. It will first describe how to accumulate such responses and then provide a parsing example. The example script that will be walked through for this section is ble_command_get_state.py. We will be creating a small Response class that will be re-used for future tutorials. Accumulating the Response The first step is to accumulate the multiple packets into one response. Whereas for all tutorials until now, we have just used the header bytes of the response as the length, we now must completely parse the header as it is defined: 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 algorithm here (which is implemented in the Message.accumulate method) is as follows: Continuation bit set? python kotlin 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, continuation bit was not set. So 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 parsing if there are no bytes remaining. python kotlin if response.is_received: response.parse() rsp.accumulate(data) if (rsp.isReceived) { rsp.parse() ... 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 All Setting Values Query. Queries aren’t introduced until the next tutorial so for now, just pay attention to the response. We send the command as such: python kotlin QUERY_REQ_UUID = GOPRO_BASE_UUID.format(\"0076\") event.clear() await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x01, 0x12])) await event.wait() Wait to receive the notification response val getCameraSettings = ubyteArrayOf(0x01U, 0x12U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, getCameraSettings) val settings = receivedResponse.receive() Then, in the notification handler, we continuously receive and accumulate packets until we have received the entire response, at which point we notify the writer that the response is ready: python kotlin def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() Notify writer that procedure is complete event.set() private fun tlvResponseNotificationHandler(characteristic: UUID, data: UByteArray) { ... rsp.accumulate(data) if (rsp.isReceived) { rsp.parse() // Notify the command sender the the procedure is complete response = null // Clear for next command CoroutineScope(Dispatchers.IO).launch { receivedResponse.send(rsp) } } We also first parse the response but that will be described in the next section. We can see the individual packets being accumulated in the log: python kotlin INFO:root:Getting the camera's settings... INFO:root:Received response at handle=62: b'21:25:12:00:02:01:09:03:01:01:05:0 INFO:root:self.bytes_remaining=275 INFO:root:Received response at handle=62: b'80:01:00:18:01:00:1e:04:00:00:00:0 INFO:root:self.bytes_remaining=256 INFO:root:Received response at handle=62: b'81:0a:25:01:00:29:01:09:2a:01:05:2 INFO:root:self.bytes_remaining=237 INFO:root:Received response at handle=62: b'82:2f:01:04:30:01:03:36:01:00:3b:0 INFO:root:self.bytes_remaining=218 INFO:root:Received response at handle=62: b'83:04:00:00:00:00:3e:04:00:00:00:0 INFO:root:self.bytes_remaining=199 INFO:root:Received response at handle=62: b'84:00:42:04:00:00:00:00:43:04:00:0 INFO:root:self.bytes_remaining=180 INFO:root:Received response at handle=62: b'85:4f:01:00:53:01:00:54:01:00:55:0 INFO:root:self.bytes_remaining=161 INFO:root:Received response at handle=62: b'86:01:28:5b:01:02:60:01:00:66:01:0 INFO:root:self.bytes_remaining=142 INFO:root:Received response at handle=62: b'87:00:6a:01:00:6f:01:0a:70:01:ff:7 INFO:root:self.bytes_remaining=123 INFO:root:Received response at handle=62: b'88:75:01:00:76:01:04:79:01:00:7a:0 INFO:root:self.bytes_remaining=104 INFO:root:Received response at handle=62: b'89:01:00:7e:01:00:80:01:0c:81:01:0 INFO:root:self.bytes_remaining=85 INFO:root:Received response at handle=62: b'8a:0c:85:01:09:86:01:00:87:01:01:8 INFO:root:self.bytes_remaining=66 INFO:root:Received response at handle=62: b'8b:92:01:00:93:01:00:94:01:02:95:0 INFO:root:self.bytes_remaining=47 INFO:root:Received response at handle=62: b'8c:01:00:9c:01:00:9d:01:00:9e:01:0 INFO:root:self.bytes_remaining=28 INFO:root:Received response at handle=62: b'8d:00:a2:01:00:a3:01:01:a4:01:00:a INFO:root:self.bytes_remaining=9 INFO:root:Received response at handle=62: b'8e:a8:04:00:00:00:00:a9:01:01' INFO:root:self.bytes_remaining=0 INFO:root:Successfully received the response Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 01:12 Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 21:2B:12:00:02:01:04:03:01:05:05:01:00:06:01:01:0D:01:01:13 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 21:2B:12:00:02:01:04:03:01:05:05:01:00:06:01:01:0D:01:01:13 Received packet of length 18. 281 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 80:01:00:18:01:00:1E:04:00:00:00:6E:1F:01:00:20:04:00:00:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 80:01:00:18:01:00:1E:04:00:00:00:6E:1F:01:00:20:04:00:00:00 Received packet of length 19. 262 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 81:0A:25:01:00:29:01:09:2A:01:08:2B:01:00:2C:01:09:2D:01:08 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 81:0A:25:01:00:29:01:09:2A:01:08:2B:01:00:2C:01:09:2D:01:08 Received packet of length 19. 243 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 82:2F:01:07:36:01:01:3B:01:04:3C:04:00:00:00:00:3D:04:00:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 82:2F:01:07:36:01:01:3B:01:04:3C:04:00:00:00:00:3D:04:00:00 Received packet of length 19. 224 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 83:00:00:3E:04:00:12:4F:80:40:01:04:41:04:00:00:00:00:42:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 83:00:00:3E:04:00:12:4F:80:40:01:04:41:04:00:00:00:00:42:04 Received packet of length 19. 205 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 84:00:00:00:00:43:04:00:12:4F:80:4B:01:00:4C:01:00:53:01:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 84:00:00:00:00:43:04:00:12:4F:80:4B:01:00:4C:01:00:53:01:01 Received packet of length 19. 186 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 85:54:01:00:55:01:00:56:01:00:57:01:00:58:01:32:5B:01:03:66 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 85:54:01:00:55:01:00:56:01:00:57:01:00:58:01:32:5B:01:03:66 Received packet of length 19. 167 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 86:01:08:67:01:03:69:01:00:6F:01:0A:70:01:64:72:01:01:73:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 86:01:08:67:01:03:69:01:00:6F:01:0A:70:01:64:72:01:01:73:01 Received packet of length 19. 148 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 87:00:74:01:02:75:01:01:76:01:04:79:01:03:7A:01:65:7B:01:65 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 87:00:74:01:02:75:01:01:76:01:04:79:01:03:7A:01:65:7B:01:65 Received packet of length 19. 129 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 88:7C:01:64:7D:01:00:7E:01:00:80:01:0D:81:01:02:82:01:69:83 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 88:7C:01:64:7D:01:00:7E:01:00:80:01:0D:81:01:02:82:01:69:83 Received packet of length 19. 110 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 89:01:03:84:01:0C:86:01:02:87:01:01:8B:01:03:90:01:0C:91:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 89:01:03:84:01:0C:86:01:02:87:01:01:8B:01:03:90:01:0C:91:01 Received packet of length 19. 91 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8A:00:92:01:00:93:01:00:94:01:01:95:01:02:96:01:00:97:01:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8A:00:92:01:00:93:01:00:94:01:01:95:01:02:96:01:00:97:01:00 Received packet of length 19. 72 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8B:99:01:64:9A:01:02:9B:01:64:9C:01:64:9D:01:64:9E:01:01:9F Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8B:99:01:64:9A:01:02:9B:01:64:9C:01:64:9D:01:64:9E:01:01:9F Received packet of length 19. 53 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8C:01:01:A0:01:00:A1:01:64:A2:01:00:A3:01:01:A4:01:64:A7:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8C:01:01:A0:01:00:A1:01:64:A2:01:00:A3:01:01:A4:01:64:A7:01 Received packet of length 19. 34 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8D:04:A8:04:00:00:00:00:A9:01:01:AE:01:00:AF:01:01:B0:01:03 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8D:04:A8:04:00:00:00:00:A9:01:01:AE:01:00:AF:01:01:B0:01:03 Received packet of length 19. 15 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8E:B1:01:00:B2:01:01:B3:01:03:B4:01:00:B5:01:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8E:B1:01:00:B2:01:01:B3:01:03:B4:01:00:B5:01:00 Received packet of length 15. 0 bytes remaining Received the expected successful response Got the camera's settings successfully At this point the response has been accumulated. See the next section for how to parse it. 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 😜. Parsing a Query Response This section is going to describe responses to to BLE status / setting queries. We don’t actually introduce such queries until the next tutorial so for now, only the parsing of the response is important. While multi-packet responses are almost always Query Responses, they can also be from Command Complex responses. In a real-world implementation, it is therefore necessary to check the received UUID to see how to parse. Query Responses contain one or more TLV groups in their Response data. To recap, the generic response format is: Header (length) Query ID Status Response 1-2 bytes 1 byte 1 bytes Length - 2 bytes This means that query responses will contain an array of additional TLV groups in the “Response” field as such: ID1 Length1 Value1 ID2 Length2 Value 2 … IDN LengthN ValueN 1 byte 1 byte Length1 bytes 1 byte 1 byte Length2 bytes … 1 byte 1 byte LengthN bytes Depending on the amount of query results in the response, this response can be one or multiple packets. Therefore, we need to account for the possibility that it may always be more than 1 packet. We can see an example of such parsing in the response parse method as shown below: We have already parsed the length when we were accumulating the packet. So the next step is to parse the Query ID and Status: python kotlin self.id = self.bytes[0] self.status = self.bytes[1] id = packet[0].toInt() status = packet[1].toInt() We then continuously parse Type (ID) - Length - Value groups until we have consumed the response. We are storing each value in a hash map indexed by ID for later access. python kotlin buf = self.bytes[2:] while len(buf) > 0: Get ID and Length param_id = buf[0] param_len = buf[1] buf = buf[2:] Get the value value = buf[:param_len] Store in dict for later access self.data[param_id] = value Advance the buffer buf = buf[param_len:] 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 In the tutorial demo, we then log this entire dict after parsing is complete as such (abbreviated for brevity): python kotlin INFO:root:Received settings : { \"2\": \"09\", \"3\": \"01\", \"5\": \"00\", \"6\": \"01\", \"13\": \"01\", \"19\": \"00\", \"30\": \"00:00:00:00\", \"31\": \"00\", \"32\": \"00:00:00:0a\", \"41\": \"09\", \"42\": \"05\", \"43\": \"00\", ... \"160\": \"00\", \"161\": \"00\", \"162\": \"00\", \"163\": \"01\", \"164\": \"00\", \"165\": \"00\", \"166\": \"00\", \"167\": \"04\", \"168\": \"00:00:00:00\", \"169\": \"01\" } { \"2\": \"09\", \"3\": \"01\", \"5\": \"00\", \"6\": \"01\", \"13\": \"01\", \"19\": \"00\", \"24\": \"00\", \"30\": \"00:00:00:6E\", \"31\": \"00\", \"32\": \"00:00:00:0A\", \"37\": \"00\", \"41\": \"09\", \"42\": \"08\", \"43\": \"00\", \"44\": \"09\", \"45\": \"08\", \"47\": \"07\", ... \"115\": \"00\", \"116\": \"02\", \"117\": \"01\", \"151\": \"00\", \"153\": \"64\", \"154\": \"02\", \"155\": \"64\", \"156\": \"64\", \"157\": \"64\", \"158\": \"01\", \"159\": \"01\", \"160\": \"00\", \"161\": \"64\", \"162\": \"00\", \"163\": \"01\", \"164\": \"64\", \"167\": \"04\", \"168\": \"00:00:00:00\", \"169\": \"01\", \"174\": \"00\", \"175\": \"01\", \"176\": \"03\", \"177\": \"00\", \"178\": \"01\", \"179\": \"03\", \"180\": \"00\", \"181\": \"00\" } We can see what each of these values mean by looking at the Open GoPro Interface. For example: ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 How many packets are query responses? A: Always 1 packet B: Always multiple packets C: Always 1 packet except for complex responses D: Can be 1 or multiple packets Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. Query responses can be one packet (if for example querying a specific setting) or multiple packets (when querying many or all settings as in the example here). See the next tutorial for more information on queries. Which field is not common to all responses? A: length B: status C: ID D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. Query responses can be one packet (if for example querying a specific setting) or multiple packets (when querying many or all settings as in the example here). See the next tutorial for more information on queries. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now parse any TLV response that is received from the GoPro, at least if it is 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 more about 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 commands instead return protobuf responses. These are not considered here and will be discussed in a future tutorial. It is suggested that you have first completed the connect and sending commands tutorials before going through this tutorial. This tutorial will give an overview of types of responses, then give examples of parsing each type before finally providing a Response class that will be used in future tutorials. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x 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_state.py See the help for parameter definitions: $ python ble_command_get_state.py --help usage: ble_command_get_state.py [-h] [-i IDENTIFIER] Connect to a GoPro camera via BLE, then get its statuses and settings. 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 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 connect 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 sending specific commands 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 Open GoPro Interface, 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 in [Parsing Multiple Packet TLV Responses]. Parsing a One Packet TLV Response This section will describe how to parse one packet (<= 20 byte) responses. A one-packet response is formatted as such: Header (length) Command / Setting ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes Command / Setting Responses with Response 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 / Setting / Status 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. Complex Command Response There are some commands that do return additional response data. These are called “complex responses.” From the commands reference, we can see that these are: Get Open GoPro Version (ID == 0x51) Get Hardware Info (ID == 0x3C) In this tutorial, we will walk through creating a simple parser to parse the Open GoPro Get Version Command. 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 command to the Command Request UUID: python kotlin COMMAND_REQ_UUID = GOPRO_BASE_UUID.format(\"0072\") event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x01, 0x51])) await event.wait() Wait to receive the notification response We then receive a response at the expected handle. This is logged as: INFO:root:Getting the Open GoPro version... INFO:root:Received response at handle=52: b'06:51:00:01:02:01:00' val getVersion = ubyteArrayOf(0x01U, 0x51U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, getVersion) val version = receivedResponse.receive() as Response.Complex // Wait to receive response This is loged 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 b5f90073-aa8d-11e3-9046-0002a5d5c51b: 06:51:00:01:02:01:00 This response equates to: Header (length) Command / Setting / Status ID Status Response 1 byte 1 byte 1 bytes Length - 2 bytes 0x06 0x51 == Get Version 0x00 == Success 0x01 0x02 0x01 0x00 We can see that this “complex response” contains 4 additional bytes that need to be parsed. Using the information from the interface description, we know to parse this as: Byte Meaning 0x01 Length of Major Version Number 0x02 Major Version Number 0x01 Length of Minor Version Number 0x00 Minor Version Number We implement this in the notification handler as follows. First, we parse the length, command ID, and status from the first 3 bytes of the response. Then we parse the remaining four bytes of the response as individual values formatted as such: Length Value 1 byte Length bytes python kotlin The snippets of code included in this section are taken from the notification handler Parse first 3 bytes len = data[0] command_id = data[1] status = data[2] Parse remaining four bytes index = 3 params = [] while index <= len: param_len = data[index] index += 1 params.append(data[index : index + param_len]) index += param_len The snippets of code included in this section are taken from the Response.Complex parse method. For the contrived code in this tutorial, we have separate Response sealed classes to handle each use case. // Parse header bytes id = packet[0].toInt() status = packet[1].toInt() var buf = packet.drop(2) // Parse remaining packet while (buf.isNotEmpty()) { // Get each parameter's ID and length val paramLen = buf[0].toInt() buf = buf.drop(1) // Get the parameter's value val paramVal = buf.take(paramLen) // Store in data list data += paramVal.toUByteArray() // Advance the buffer for continued parsing buf = buf.drop(paramLen) } From the complex 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, minor = params logger.info(f\"Received a response to {command_id=} with {status=}: version={major[0]}.{minor[0]}\") which shows on the log as: INFO:root:Received a response to command_id=81 with status=0: version=2.0 val version = receivedResponse.receive() as Response.Complex // Wait to receive response val major = version.data[0].first().toInt() val minor = version.data[1].first().toInt() Timber.i(\"Got the Open GoPro version successfully: $major.$minor\") which shows on the log as such: Got the Open GoPro version successfully: 2.0 Quiz time! 📚 ✏️ What is the maximum size of an individual notification response packet? 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 packets 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. What is the maximum amount of packets that one response can be composed of? A: Always 1 packet B: Always multiple packets. C: Always 1 packet except for complex responses. Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. Command responses are almost always 1 packet (just returning the status). The exception are complex responses which can be multiple packets (in the case of Get Hardware Info) How many packets are setting responses comprised of? A: Always 1 packet B: Always multiple packets. C: Always 1 packet except for complex responses. Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. Settings Responses only ever contain the command status. Furthermore, there is no concept of complex responses for setting commands. Parsing Multiple Packet TLV Responses This section will describe parsing TLV responses that contain more than one packet. It will first describe how to accumulate such responses and then provide a parsing example. The example script that will be walked through for this section is ble_command_get_state.py. We will be creating a small Response class that will be re-used for future tutorials. Accumulating the Response The first step is to accumulate the multiple packets into one response. Whereas for all tutorials until now, we have just used the header bytes of the response as the length, we now must completely parse the header as it is defined: 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 algorithm here (which is implemented in the Message.accumulate method) is as follows: Continuation bit set? python kotlin 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, continuation bit was not set. So 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 parsing if there are no bytes remaining. python kotlin if response.is_received: response.parse() rsp.accumulate(data) if (rsp.isReceived) { rsp.parse() ... 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 All Setting Values Query. Queries aren’t introduced until the next tutorial so for now, just pay attention to the response. We send the command as such: python kotlin QUERY_REQ_UUID = GOPRO_BASE_UUID.format(\"0076\") event.clear() await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x01, 0x12])) await event.wait() Wait to receive the notification response val getCameraSettings = ubyteArrayOf(0x01U, 0x12U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, getCameraSettings) val settings = receivedResponse.receive() Then, in the notification handler, we continuously receive and accumulate packets until we have received the entire response, at which point we notify the writer that the response is ready: python kotlin def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() Notify writer that procedure is complete event.set() private fun tlvResponseNotificationHandler(characteristic: UUID, data: UByteArray) { ... rsp.accumulate(data) if (rsp.isReceived) { rsp.parse() // Notify the command sender the the procedure is complete response = null // Clear for next command CoroutineScope(Dispatchers.IO).launch { receivedResponse.send(rsp) } } We also first parse the response but that will be described in the next section. We can see the individual packets being accumulated in the log: python kotlin INFO:root:Getting the camera's settings... INFO:root:Received response at handle=62: b'21:25:12:00:02:01:09:03:01:01:05:0 INFO:root:self.bytes_remaining=275 INFO:root:Received response at handle=62: b'80:01:00:18:01:00:1e:04:00:00:00:0 INFO:root:self.bytes_remaining=256 INFO:root:Received response at handle=62: b'81:0a:25:01:00:29:01:09:2a:01:05:2 INFO:root:self.bytes_remaining=237 INFO:root:Received response at handle=62: b'82:2f:01:04:30:01:03:36:01:00:3b:0 INFO:root:self.bytes_remaining=218 INFO:root:Received response at handle=62: b'83:04:00:00:00:00:3e:04:00:00:00:0 INFO:root:self.bytes_remaining=199 INFO:root:Received response at handle=62: b'84:00:42:04:00:00:00:00:43:04:00:0 INFO:root:self.bytes_remaining=180 INFO:root:Received response at handle=62: b'85:4f:01:00:53:01:00:54:01:00:55:0 INFO:root:self.bytes_remaining=161 INFO:root:Received response at handle=62: b'86:01:28:5b:01:02:60:01:00:66:01:0 INFO:root:self.bytes_remaining=142 INFO:root:Received response at handle=62: b'87:00:6a:01:00:6f:01:0a:70:01:ff:7 INFO:root:self.bytes_remaining=123 INFO:root:Received response at handle=62: b'88:75:01:00:76:01:04:79:01:00:7a:0 INFO:root:self.bytes_remaining=104 INFO:root:Received response at handle=62: b'89:01:00:7e:01:00:80:01:0c:81:01:0 INFO:root:self.bytes_remaining=85 INFO:root:Received response at handle=62: b'8a:0c:85:01:09:86:01:00:87:01:01:8 INFO:root:self.bytes_remaining=66 INFO:root:Received response at handle=62: b'8b:92:01:00:93:01:00:94:01:02:95:0 INFO:root:self.bytes_remaining=47 INFO:root:Received response at handle=62: b'8c:01:00:9c:01:00:9d:01:00:9e:01:0 INFO:root:self.bytes_remaining=28 INFO:root:Received response at handle=62: b'8d:00:a2:01:00:a3:01:01:a4:01:00:a INFO:root:self.bytes_remaining=9 INFO:root:Received response at handle=62: b'8e:a8:04:00:00:00:00:a9:01:01' INFO:root:self.bytes_remaining=0 INFO:root:Successfully received the response Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 01:12 Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 21:2B:12:00:02:01:04:03:01:05:05:01:00:06:01:01:0D:01:01:13 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 21:2B:12:00:02:01:04:03:01:05:05:01:00:06:01:01:0D:01:01:13 Received packet of length 18. 281 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 80:01:00:18:01:00:1E:04:00:00:00:6E:1F:01:00:20:04:00:00:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 80:01:00:18:01:00:1E:04:00:00:00:6E:1F:01:00:20:04:00:00:00 Received packet of length 19. 262 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 81:0A:25:01:00:29:01:09:2A:01:08:2B:01:00:2C:01:09:2D:01:08 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 81:0A:25:01:00:29:01:09:2A:01:08:2B:01:00:2C:01:09:2D:01:08 Received packet of length 19. 243 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 82:2F:01:07:36:01:01:3B:01:04:3C:04:00:00:00:00:3D:04:00:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 82:2F:01:07:36:01:01:3B:01:04:3C:04:00:00:00:00:3D:04:00:00 Received packet of length 19. 224 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 83:00:00:3E:04:00:12:4F:80:40:01:04:41:04:00:00:00:00:42:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 83:00:00:3E:04:00:12:4F:80:40:01:04:41:04:00:00:00:00:42:04 Received packet of length 19. 205 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 84:00:00:00:00:43:04:00:12:4F:80:4B:01:00:4C:01:00:53:01:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 84:00:00:00:00:43:04:00:12:4F:80:4B:01:00:4C:01:00:53:01:01 Received packet of length 19. 186 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 85:54:01:00:55:01:00:56:01:00:57:01:00:58:01:32:5B:01:03:66 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 85:54:01:00:55:01:00:56:01:00:57:01:00:58:01:32:5B:01:03:66 Received packet of length 19. 167 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 86:01:08:67:01:03:69:01:00:6F:01:0A:70:01:64:72:01:01:73:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 86:01:08:67:01:03:69:01:00:6F:01:0A:70:01:64:72:01:01:73:01 Received packet of length 19. 148 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 87:00:74:01:02:75:01:01:76:01:04:79:01:03:7A:01:65:7B:01:65 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 87:00:74:01:02:75:01:01:76:01:04:79:01:03:7A:01:65:7B:01:65 Received packet of length 19. 129 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 88:7C:01:64:7D:01:00:7E:01:00:80:01:0D:81:01:02:82:01:69:83 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 88:7C:01:64:7D:01:00:7E:01:00:80:01:0D:81:01:02:82:01:69:83 Received packet of length 19. 110 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 89:01:03:84:01:0C:86:01:02:87:01:01:8B:01:03:90:01:0C:91:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 89:01:03:84:01:0C:86:01:02:87:01:01:8B:01:03:90:01:0C:91:01 Received packet of length 19. 91 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8A:00:92:01:00:93:01:00:94:01:01:95:01:02:96:01:00:97:01:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8A:00:92:01:00:93:01:00:94:01:01:95:01:02:96:01:00:97:01:00 Received packet of length 19. 72 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8B:99:01:64:9A:01:02:9B:01:64:9C:01:64:9D:01:64:9E:01:01:9F Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8B:99:01:64:9A:01:02:9B:01:64:9C:01:64:9D:01:64:9E:01:01:9F Received packet of length 19. 53 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8C:01:01:A0:01:00:A1:01:64:A2:01:00:A3:01:01:A4:01:64:A7:01 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8C:01:01:A0:01:00:A1:01:64:A2:01:00:A3:01:01:A4:01:64:A7:01 Received packet of length 19. 34 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8D:04:A8:04:00:00:00:00:A9:01:01:AE:01:00:AF:01:01:B0:01:03 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8D:04:A8:04:00:00:00:00:A9:01:01:AE:01:00:AF:01:01:B0:01:03 Received packet of length 19. 15 bytes remaining Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8E:B1:01:00:B2:01:01:B3:01:03:B4:01:00:B5:01:00 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8E:B1:01:00:B2:01:01:B3:01:03:B4:01:00:B5:01:00 Received packet of length 15. 0 bytes remaining Received the expected successful response Got the camera's settings successfully At this point the response has been accumulated. See the next section for how to parse it. 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 😜. Parsing a Query Response This section is going to describe responses to to BLE status / setting queries. We don’t actually introduce such queries until the next tutorial so for now, only the parsing of the response is important. While multi-packet responses are almost always Query Responses, they can also be from Command Complex responses. In a real-world implementation, it is therefore necessary to check the received UUID to see how to parse. Query Responses contain one or more TLV groups in their Response data. To recap, the generic response format is: Header (length) Query ID Status Response 1-2 bytes 1 byte 1 bytes Length - 2 bytes This means that query responses will contain an array of additional TLV groups in the “Response” field as such: ID1 Length1 Value1 ID2 Length2 Value 2 … IDN LengthN ValueN 1 byte 1 byte Length1 bytes 1 byte 1 byte Length2 bytes … 1 byte 1 byte LengthN bytes Depending on the amount of query results in the response, this response can be one or multiple packets. Therefore, we need to account for the possibility that it may always be more than 1 packet. We can see an example of such parsing in the response parse method as shown below: We have already parsed the length when we were accumulating the packet. So the next step is to parse the Query ID and Status: python kotlin self.id = self.bytes[0] self.status = self.bytes[1] id = packet[0].toInt() status = packet[1].toInt() We then continuously parse Type (ID) - Length - Value groups until we have consumed the response. We are storing each value in a hash map indexed by ID for later access. python kotlin buf = self.bytes[2:] while len(buf) > 0: Get ID and Length param_id = buf[0] param_len = buf[1] buf = buf[2:] Get the value value = buf[:param_len] Store in dict for later access self.data[param_id] = value Advance the buffer buf = buf[param_len:] 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 In the tutorial demo, we then log this entire dict after parsing is complete as such (abbreviated for brevity): python kotlin INFO:root:Received settings : { \"2\": \"09\", \"3\": \"01\", \"5\": \"00\", \"6\": \"01\", \"13\": \"01\", \"19\": \"00\", \"30\": \"00:00:00:00\", \"31\": \"00\", \"32\": \"00:00:00:0a\", \"41\": \"09\", \"42\": \"05\", \"43\": \"00\", ... \"160\": \"00\", \"161\": \"00\", \"162\": \"00\", \"163\": \"01\", \"164\": \"00\", \"165\": \"00\", \"166\": \"00\", \"167\": \"04\", \"168\": \"00:00:00:00\", \"169\": \"01\" } { \"2\": \"09\", \"3\": \"01\", \"5\": \"00\", \"6\": \"01\", \"13\": \"01\", \"19\": \"00\", \"24\": \"00\", \"30\": \"00:00:00:6E\", \"31\": \"00\", \"32\": \"00:00:00:0A\", \"37\": \"00\", \"41\": \"09\", \"42\": \"08\", \"43\": \"00\", \"44\": \"09\", \"45\": \"08\", \"47\": \"07\", ... \"115\": \"00\", \"116\": \"02\", \"117\": \"01\", \"151\": \"00\", \"153\": \"64\", \"154\": \"02\", \"155\": \"64\", \"156\": \"64\", \"157\": \"64\", \"158\": \"01\", \"159\": \"01\", \"160\": \"00\", \"161\": \"64\", \"162\": \"00\", \"163\": \"01\", \"164\": \"64\", \"167\": \"04\", \"168\": \"00:00:00:00\", \"169\": \"01\", \"174\": \"00\", \"175\": \"01\", \"176\": \"03\", \"177\": \"00\", \"178\": \"01\", \"179\": \"03\", \"180\": \"00\", \"181\": \"00\" } We can see what each of these values mean by looking at the Open GoPro Interface. For example: ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 How many packets are query responses? A: Always 1 packet B: Always multiple packets C: Always 1 packet except for complex responses D: Can be 1 or multiple packets Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. Query responses can be one packet (if for example querying a specific setting) or multiple packets (when querying many or all settings as in the example here). See the next tutorial for more information on queries. Which field is not common to all responses? A: length B: status C: ID D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. Query responses can be one packet (if for example querying a specific setting) or multiple packets (when querying many or all settings as in the example here). See the next tutorial for more information on queries. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now parse any TLV response that is received from the GoPro, at least if it is 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 more about queries, go to the next tutorial.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/parse-ble-responses#" }, { "title": "Tutorial 4: BLE Queries: ", - "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to query the camera’s setting and status information via BLE. “Queries” in this sense are specifically procedures that: are initiated by writing to the Query UUID receive responses via the Query Response UUID. This will be described in more detail below. It is suggested that you have first completed the connect, sending commands, and parsing responses tutorials before going through this tutorial. This tutorial only considers sending these queries as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future lab. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x 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_command_poll_resolution_value.py See the help for parameter definitions: $ python ble_command_poll_resolution_value.py --help usage: ble_command_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_command_poll_multiple_setting_values.py See the help for parameter definitions: $ python ble_command_poll_multiple_setting_values.py --help usage: ble_command_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_command_register_resolution_value_updates.py See the help for parameter definitions: $ python ble_command_register_resolution_value_updates.py --help usage: ble_command_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 connect tutorial. We will also be using the Response class that was defined in the parsing responses tutorial to accumulate and parse notification responses to the Query Response characteristic. Throughout this tutorial, the query information that we will be reading is the Resolution Setting (ID 0x02). python kotlin Therefore, we have slightly changed the notification handler to update a global resolution variable as it queries the resolution: def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) Notify writer that the procedure is complete event.set() Therefore, we have slightly updated the notification handler to only handle query responses: fun resolutionPollingNotificationHandler(characteristic: UUID, data: UByteArray) { GoProUUID.fromUuid(characteristic)?.let { // If response is currently empty, create a new one response = response ?: Response.Query() // We're only handling queries in this tutorial } ?: return // We don't care about non-GoPro characteristics (i.e. the BT Core Battery service) Timber.d(\"Received response on $characteristic: ${data.toHexString()}\") response?.let { rsp -> rsp.accumulate(data) if (rsp.isReceived) { rsp.parse() // If this is a query response, it must contain a resolution value if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { Timber.i(\"Received resolution query response\") } ... We are also defining a resolution enum that will be updated as we receive new resolutions: 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 Polling Query Information It is possible to poll one or more setting / status values using the following commands: Query ID Request Query 0x12 Get Setting value(s) len:12:xx:xx 0x13 Get Status value(s) 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 commands, combination of settings / statuses can not be polled simultaneously. Here is a generic sequence diagram (the same is true for statuses): Open GoPro user deviceGoProConnected (steps from connect tutorial)Get Setting value(s) command written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUIDOpen GoPro user deviceGoPro 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 command: python kotlin The sample code can be found in in ble_query_poll_resolution_value.py. Let’s first define the UUID’s to write to and receive from: QUERY_REQ_UUID = GOPRO_BASE_UUID.format(\"0076\") QUERY_RSP_UUID = GOPRO_BASE_UUID.format(\"0077\") Then actually send the command: event.clear() await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x12, RESOLUTION_ID])) await event.wait() Wait to receive the notification response val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) When the response is received in / from the notification handler, we update the global resolution variable: python kotlin def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) Notify the writer if we have received the entire response if response.is_received: response.parse() If this is query response, it must contain a resolution value if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) which logs as such: INFO:root:Getting the current resolution INFO:root:Received response at handle=62: b'05:12:00:02:01:09' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is currently Resolution.RES_1080 // Wait to receive the response and then convert it to resolution resolution = Resolution.fromValue( receivedResponse.receive().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:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:12:00:02:01:04 Received packet of length 5. 0 bytes remaining Received resolution query response Camera resolution is RES_2_7K For verification purposes, we are then changing the resolution and polling again to verify that the setting has changed: python kotlin INFO:root:Changing the resolution to Resolution.RES_2_7K... INFO:root:Received response at handle=57: b'02:02:00' INFO:root:self.bytes_remaining=0 INFO:root:Command sent successfully INFO:root:Polling the resolution to see if it has changed... INFO:root:Received response at handle=62: b'05:12:00:02:01:07' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is currently Resolution.RES_2_7K while (resolution != newResolution) { ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) resolution = Resolution.fromValue( receivedResponse.receive().data.getValue(RESOLUTION_ID).first() ) Timber.i(\"Camera resolution is currently $resolution\") } which logs as such: Changing the resolution to RES_1080 Writing characteristic b5f90074-aa8d-11e3-9046-0002a5d5c51b ==> 03:02:01:09 Wrote characteristic b5f90074-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90075-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:02:00 Received response on b5f90075-aa8d-11e3-9046-0002a5d5c51b: 02:02:00 Command sent successfully Resolution successfully changed Polling the resolution until it changes Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 02:12:02 Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:12:00:02:01:09 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:12:00:02:01:09 Received resolution query response Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b Camera resolution is currently RES_1080 Multiple Simultaneous Query Polls Rather than just polling one setting, it is also possible to poll multiple settings. An example of this is shown below. It is very similar to the previous example except for the following: The query command now includes 3 settings: Resolution, FPS, and FOV. python kotlin RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x04, 0x12, RESOLUTION_ID, FPS_ID, FOV_ID])) TODO The length (first byte of the command) has been increased to 4 to accommodate the extra settings We are also parsing the response to get all 3 values: python kotlin def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) fps = FPS(response.data[FPS_ID][0]) video_fov = VideoFOV(response.data[FOV_ID][0]) 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 INFO:root:Received response at handle=62: b'0b:12:00:02:01:07:03:01:01:79:01:00' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is currently Resolution.RES_2_7K INFO:root:Video FOV is currently VideoFOV.FOV_WIDE INFO:root:FPS is currently FPS.FPS_120 TODO Query All It is also possible to query all settings / statuses by not passing any ID’s into the the query command, i.e.: Query ID Request Query 0x12 Get All Settings 01:12 0x13 Get All Statuses 01:13 An example of this can be seen in the parsing query responses tutorial Quiz time! 📚 ✏️ How can we poll the encoding status and the resolution setting using one command? A: Concatenate a &8216;Get Setting Value&8217; command and a &8216;Get Status&8217; command with the relevant ID&8217;s B: Concatenate the &8216;Get All Setting&8217; and &8216;Get All Status&8217; commands. C: It is not possible Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. It is not possible to concatenate commands. 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 command. The Get Setting command (with resolution ID) and Get Status command(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 commands are: Query ID Request Query 0x52 Register updates for setting(s) len:52:xx:xx 0x53 Register updates for status(es) len:53:xx:xx 0x72 Unregister updates for setting(s) len:72:xx:xx 0x73 Unregister updates for status(es) len:73:xx:xx 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): Open GoPro user deviceGoProConnected (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 changesOpen GoPro user deviceGoPro 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 command 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 First, let’s define the UUID’s we will be using: SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format(\"0074\") SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format(\"0075\") QUERY_REQ_UUID = GOPRO_BASE_UUID.format(\"0076\") QUERY_RSP_UUID = GOPRO_BASE_UUID.format(\"0077\") Then, let’s send the register BLE message… event.clear() await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x52, RESOLUTION_ID])) await event.wait() Wait to receive the notification response 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 def notification_handler(handle: int, data: bytes) -> None: logger.info(f'Received response at {handle=}: {hexlify(data, \":\")!r}') response.accumulate(data) Notify the writer if we have received the entire response if response.is_received: response.parse() If this is query response, it must contain a resolution value if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: global resolution resolution = Resolution(response.data[RESOLUTION_ID][0]) This will show in the log as such: INFO:root:Registering for resolution updates INFO:root:Received response at handle=62: b'05:52:00:02:01:07' INFO:root:self.bytes_remaining=0 INFO:root:Successfully registered for resolution value updates. INFO:root:Resolution is currently Resolution.RES_2_7K fun resolutionRegisteringNotificationHandler(characteristic: UUID, data: UByteArray) { ... if (rsp.isReceived) { rsp.parse() if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { Timber.i(\"Received resolution query response\") resolution = Resolution.fromValue(rsp.data.getValue(RESOLUTION_ID).first()) Timber.i(\"Resolution is now $resolution\") ... 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 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. python kotlin This will show in the log as such: INFO:root:Successfully changed the resolution INFO:root:Received response at handle=62: b'05:92:00:02:01:09' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is now Resolution.RES_1080 val newResolution = if (resolution == Resolution.RES_2_7K) Resolution.RES_1080 else Resolution.RES_2_7K val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, newResolution.value) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_SETTING.uuid, setResolution) val setResolutionResponse = receivedResponse.receive() // Verify we receive the update from the camera when the resolution changes while (resolution != newResolution) { receivedResponse.receive() } 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. If you have been following these tutorials in order, here is an extra 🥇🍾 Congratulations 🍰👍 because you have completed all of the BLE tutorials. Next, to get started with WiFI (specifically to enable and connect to it), proceed to the next tutorial.", + "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to query the camera’s setting and status information via BLE. “Queries” in this sense are specifically procedures that: are initiated by writing to the Query UUID receive responses via the Query Response UUID. This will be described in more detail below. It is suggested that you have first completed the connect, sending commands, and parsing responses tutorials before going through this tutorial. This tutorial only considers sending these queries as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future lab. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x 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_command_poll_resolution_value.py See the help for parameter definitions: $ python ble_command_poll_resolution_value.py --help usage: ble_command_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_command_poll_multiple_setting_values.py See the help for parameter definitions: $ python ble_command_poll_multiple_setting_values.py --help usage: ble_command_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_command_register_resolution_value_updates.py See the help for parameter definitions: $ python ble_command_register_resolution_value_updates.py --help usage: ble_command_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 connect tutorial. We will also be using the Response class that was defined in the parsing responses tutorial to accumulate and parse notification responses to the Query Response characteristic. Throughout this tutorial, the query information that we will be reading is the Resolution Setting (ID 0x02). python kotlin Therefore, we have slightly changed the notification handler to update a global resolution variable as it queries the resolution: def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) Notify writer that the procedure is complete event.set() Therefore, we have slightly updated the notification handler to only handle query responses: fun resolutionPollingNotificationHandler(characteristic: UUID, data: UByteArray) { GoProUUID.fromUuid(characteristic)?.let { // If response is currently empty, create a new one response = response ?: Response.Query() // We're only handling queries in this tutorial } ?: return // We don't care about non-GoPro characteristics (i.e. the BT Core Battery service) Timber.d(\"Received response on $characteristic: ${data.toHexString()}\") response?.let { rsp -> rsp.accumulate(data) if (rsp.isReceived) { rsp.parse() // If this is a query response, it must contain a resolution value if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { Timber.i(\"Received resolution query response\") } ... We are also defining a resolution enum that will be updated as we receive new resolutions: 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 Polling Query Information It is possible to poll one or more setting / status values using the following commands: Query ID Request Query 0x12 Get Setting value(s) len:12:xx:xx 0x13 Get Status value(s) 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 commands, 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) command 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 command: python kotlin The sample code can be found in in ble_query_poll_resolution_value.py. Let’s first define the UUID’s to write to and receive from: QUERY_REQ_UUID = GOPRO_BASE_UUID.format(\"0076\") QUERY_RSP_UUID = GOPRO_BASE_UUID.format(\"0077\") Then actually send the command: event.clear() await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x12, RESOLUTION_ID])) await event.wait() Wait to receive the notification response val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) When the response is received in / from the notification handler, we update the global resolution variable: python kotlin def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) Notify the writer if we have received the entire response if response.is_received: response.parse() If this is query response, it must contain a resolution value if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) which logs as such: INFO:root:Getting the current resolution INFO:root:Received response at handle=62: b'05:12:00:02:01:09' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is currently Resolution.RES_1080 // Wait to receive the response and then convert it to resolution resolution = Resolution.fromValue( receivedResponse.receive().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:04 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:12:00:02:01:04 Received packet of length 5. 0 bytes remaining Received resolution query response Camera resolution is RES_2_7K For verification purposes, we are then changing the resolution and polling again to verify that the setting has changed: python kotlin INFO:root:Changing the resolution to Resolution.RES_2_7K... INFO:root:Received response at handle=57: b'02:02:00' INFO:root:self.bytes_remaining=0 INFO:root:Command sent successfully INFO:root:Polling the resolution to see if it has changed... INFO:root:Received response at handle=62: b'05:12:00:02:01:07' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is currently Resolution.RES_2_7K while (resolution != newResolution) { ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) resolution = Resolution.fromValue( receivedResponse.receive().data.getValue(RESOLUTION_ID).first() ) Timber.i(\"Camera resolution is currently $resolution\") } which logs as such: Changing the resolution to RES_1080 Writing characteristic b5f90074-aa8d-11e3-9046-0002a5d5c51b ==> 03:02:01:09 Wrote characteristic b5f90074-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90075-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:02:00 Received response on b5f90075-aa8d-11e3-9046-0002a5d5c51b: 02:02:00 Command sent successfully Resolution successfully changed Polling the resolution until it changes Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 02:12:02 Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:12:00:02:01:09 Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:12:00:02:01:09 Received resolution query response Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b Camera resolution is currently RES_1080 Multiple Simultaneous Query Polls Rather than just polling one setting, it is also possible to poll multiple settings. An example of this is shown below. It is very similar to the previous example except for the following: The query command now includes 3 settings: Resolution, FPS, and FOV. python kotlin RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x04, 0x12, RESOLUTION_ID, FPS_ID, FOV_ID])) TODO The length (first byte of the command) has been increased to 4 to accommodate the extra settings We are also parsing the response to get all 3 values: python kotlin def notification_handler(handle: int, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) fps = FPS(response.data[FPS_ID][0]) video_fov = VideoFOV(response.data[FOV_ID][0]) 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 INFO:root:Received response at handle=62: b'0b:12:00:02:01:07:03:01:01:79:01:00' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is currently Resolution.RES_2_7K INFO:root:Video FOV is currently VideoFOV.FOV_WIDE INFO:root:FPS is currently FPS.FPS_120 TODO Query All It is also possible to query all settings / statuses by not passing any ID’s into the the query command, i.e.: Query ID Request Query 0x12 Get All Settings 01:12 0x13 Get All Statuses 01:13 An example of this can be seen in the parsing query responses tutorial Quiz time! 📚 ✏️ How can we poll the encoding status and the resolution setting using one command? A: Concatenate a &8216;Get Setting Value&8217; command and a &8216;Get Status&8217; command with the relevant ID&8217;s B: Concatenate the &8216;Get All Setting&8217; and &8216;Get All Status&8217; commands. C: It is not possible Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. It is not possible to concatenate commands. 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 command. The Get Setting command (with resolution ID) and Get Status command(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 commands are: Query ID Request Query 0x52 Register updates for setting(s) len:52:xx:xx 0x53 Register updates for status(es) len:53:xx:xx 0x72 Unregister updates for setting(s) len:72:xx:xx 0x73 Unregister updates for status(es) len:73:xx:xx 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 command 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 First, let’s define the UUID’s we will be using: SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format(\"0074\") SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format(\"0075\") QUERY_REQ_UUID = GOPRO_BASE_UUID.format(\"0076\") QUERY_RSP_UUID = GOPRO_BASE_UUID.format(\"0077\") Then, let’s send the register BLE message… event.clear() await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x52, RESOLUTION_ID])) await event.wait() Wait to receive the notification response 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 def notification_handler(handle: int, data: bytes) -> None: logger.info(f'Received response at {handle=}: {hexlify(data, \":\")!r}') response.accumulate(data) Notify the writer if we have received the entire response if response.is_received: response.parse() If this is query response, it must contain a resolution value if client.services.characteristics[handle].uuid == QUERY_RSP_UUID: global resolution resolution = Resolution(response.data[RESOLUTION_ID][0]) This will show in the log as such: INFO:root:Registering for resolution updates INFO:root:Received response at handle=62: b'05:52:00:02:01:07' INFO:root:self.bytes_remaining=0 INFO:root:Successfully registered for resolution value updates. INFO:root:Resolution is currently Resolution.RES_2_7K fun resolutionRegisteringNotificationHandler(characteristic: UUID, data: UByteArray) { ... if (rsp.isReceived) { rsp.parse() if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { Timber.i(\"Received resolution query response\") resolution = Resolution.fromValue(rsp.data.getValue(RESOLUTION_ID).first()) Timber.i(\"Resolution is now $resolution\") ... 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 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. python kotlin This will show in the log as such: INFO:root:Successfully changed the resolution INFO:root:Received response at handle=62: b'05:92:00:02:01:09' INFO:root:self.bytes_remaining=0 INFO:root:Resolution is now Resolution.RES_1080 val newResolution = if (resolution == Resolution.RES_2_7K) Resolution.RES_1080 else Resolution.RES_2_7K val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, newResolution.value) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_SETTING.uuid, setResolution) val setResolutionResponse = receivedResponse.receive() // Verify we receive the update from the camera when the resolution changes while (resolution != newResolution) { receivedResponse.receive() } 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. If you have been following these tutorials in order, here is an extra 🥇🍾 Congratulations 🍰👍 because you have completed all of the BLE tutorials. Next, to get started with WiFI (specifically to enable and connect to it), proceed to the next tutorial.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/ble-queries#" }, { "title": "Tutorial 5: Connect WiFi: ", - "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to enable the GoPro’s WiFi Access Point (AP) so that it can be connected to. It will also provide an example of connecting to the WiFi AP. It is recommended that you have first completed the connecting, sending commands, and parsing responses tutorials before proceeding. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. The scripts that will be used for this tutorial can be found in the Tutorial 5 Folder. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements Enable WiFi AP You can test querying the current Resolution on your camera through BLE using the following script: $ python wifi_enable.py See the help for parameter definitions: $ python wifi_enable.py --help usage: wifi_enable.py [-h] [-i IDENTIFIER] [-t TIMEOUT] Connect to a GoPro camera via BLE, get WiFi info, and enable WiFi. optional arguments: -h, --help show this help message and exit -i IDENTIFIER, --identifier IDENTIFIER Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to -t TIMEOUT, --timeout TIMEOUT time in seconds to maintain connection before disconnecting. If not set, will maintain connection indefinitely The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 5” from the dropdown and click on “Perform.” This requires that a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. You can check the BLE status at the top of the app. Perform Tutorial 5 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect to BLE as was discussed in the connect tutorial. We are also using the same notification handler as was used in the sending commands tutorial Connecting to WiFi AP Now that we are connected via BLE, paired, and have enabled notifications, we can send the command to enable the WiFi AP. Here is an outline of the steps to do so: Open GoPro user deviceGoProBLEGoProWiFiScanningConnectedalt[If not Previously Paired]PairedReady to Communicateloop[Steps from Connect Tutorial]WiFi AP enabledAdvertisingAdvertisingConnectPair RequestPair ResponseEnable Notifications on Characteristic 1Enable Notifications on Characteristic 2Enable Notifications on Characteristic ..Enable Notifications on Characteristic NRead Wifi AP SSIDRead Wifi AP PasswordWrite to Enable WiFi APResponse sent as notificationConnect to WiFi APOpen GoPro user deviceGoProBLEGoProWiFi Essentially we will be finding the WiFi AP information (SSID and password) via BLE, enabling the WiFi AP via BLE, then connecting to the WiFi AP. Find WiFi Information Note that the process to get this information is different than all procedures described up to this point. Whereas the previous command, setting, and query procedures all followed the Write Request-Notification Response pattern, the WiFi Information is retrieved via direct Read Requests to BLE characteristics. Get WiFi SSID The WiFi SSID can be found by reading from the WiFi AP SSID characteristic of the WiFi Access Point service. First, let’s send the read request to get the SSID (and decode it into a string). python kotlin Let’s define the attribute to read from: WIFI_AP_SSID_UUID = GOPRO_BASE_UUID.format(\"0002\") Then send the BLE read request: ssid = await client.read_gatt_char(WIFI_AP_SSID_UUID) ssid = ssid.decode() There is no need for a synchronization event as the information is available when the read_gatt_char method returns. In the demo, this information is logged as such: INFO:root:Reading the WiFi AP SSID INFO:root:SSID is GP24500456 ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_SSID.uuid).onSuccess { ssid = it.decodeToString() } Timber.i(\"SSID is $ssid\") In the demo, this information is logged as such: Getting the SSID Read characteristic b5f90002-aa8d-11e3-9046-0002a5d5c51b : value: 64:65:62:75:67:68:65:72:6F:31:31 SSID is debughero11 Get WiFi Password The WiFi password can be found by reading from the WiFi AP password characteristic of the WiFi Access Point service. First, let’s send the read request to get the password (and decode it into a string). python kotlin Let’s define the attribute to read from: WIFI_AP_PASSWORD_UUID = GOPRO_BASE_UUID.format(\"0003\") Then send the BLE read request: There is no need for a synchronization event as the information is available when the read_gatt_char method returns. In the demo, this information is logged as such: INFO:root:Reading the WiFi AP password INFO:root:Password is g@6-Tj9-C7K ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_PASSWORD.uuid).onSuccess { password = it.decodeToString() } Timber.i(\"Password is $password\") In the demo, this information is logged as such: Getting the password Read characteristic b5f90003-aa8d-11e3-9046-0002a5d5c51b : value: 7A:33:79:2D:44:43:58:2D:50:68:6A Password is z3y-DCX-Phj Enable WiFi AP Before we can connect to the WiFi AP, we have to make sure it is enabled. This is accomplished by using the “AP Control” command: Command Bytes Ap Control Enable 0x03 0x17 0x01 0x01 Ap Control Disable 0x03 0x17 0x01 0x00 This is done in the same manner that we did in the sending commands tutorial. Now, let’s write the bytes to the “Command Request UUID” to enable the WiFi AP! python kotlin event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x03, 0x17, 0x01, 0x01])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val enableWifiCommand = ubyteArrayOf(0x03U, 0x17U, 0x01U, 0x01U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, enableWifiCommand) receivedData.receive() Note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications. This can be seen in the demo log: python kotlin INFO:root:Enabling the WiFi AP INFO:root:Received response at handle=52: b'02:17:00' INFO:root:Command sent successfully INFO:root:WiFi AP is enabled Enabling the camera's Wifi AP Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:17:01:01 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:17:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:17:00 Command sent successfully As expected, the response was received on the correct handle and the status was “success”. Establish Connection to WiFi AP python kotlin If you have been following through the ble_enable_wifi.py script, you will notice that it ends here such that we know the WiFi SSID and password and the WiFi AP is enabled and ready to connect to. This is because there are many different methods of connecting to the WiFi AP depending on your OS and the framework you are using to develop. You could, for example, simply use your OS’s WiFi GUI to connect. While out of the scope of these tutorials, there is a programmatic example of this in the cross-platform WiFi Demo from the Open GoPro Python SDK. Using the passwsord and SSID we discovered above, we will now connect to the camera’s network: wifi.connect(ssid, password) This should show a system popup on your Android device that eventually goes away once the Wifi is connected. This connection process appears to vary drastically in time. Quiz time! 📚 ✏️ How is the WiFi password response received? A: As a read response from the WiFi AP Password characteristic B: As write responses to the WiFi Request characteristic C: As notifications of the Command Response characteristic Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. This (and WiFi AP SSID) is an exception to the rule. Usually responses are received as notifications to a response characteristic. However, in this case, it is received as a direct read response (since we are reading from the characteristic and not writing to it). Which of the following statements about the GoPro WiFi AP is true? A: It only needs to be enabled once and it will then always remain on B: The WiFi password will never change C: The WiFi SSID will never change D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. While the WiFi AP will remain on for some time, it can and will eventually turn off so it is always recommended to first connect via BLE and ensure that it is enabled. The password and SSID will almost never change. However, they will change if the connections are reset via Connections->Reset Connections. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You are now connected to the GoPro’s Wifi AP and can send any of the HTTP commands defined in the Open GoPro Interface. Proceed to the next tutorial.", + "excerpt": "This document will provide a walk-through tutorial to implement the Open GoPro Interface to enable the GoPro’s WiFi Access Point (AP) so that it can be connected to. It will also provide an example of connecting to the WiFi AP. It is recommended that you have first completed the connecting, sending commands, and parsing responses tutorials before proceeding. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. The scripts that will be used for this tutorial can be found in the Tutorial 5 Folder. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements Enable WiFi AP You can test querying the current Resolution on your camera through BLE using the following script: $ python wifi_enable.py See the help for parameter definitions: $ python wifi_enable.py --help usage: wifi_enable.py [-h] [-i IDENTIFIER] [-t TIMEOUT] Connect to a GoPro camera via BLE, get WiFi info, and enable WiFi. optional arguments: -h, --help show this help message and exit -i IDENTIFIER, --identifier IDENTIFIER Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to -t TIMEOUT, --timeout TIMEOUT time in seconds to maintain connection before disconnecting. If not set, will maintain connection indefinitely The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 5” from the dropdown and click on “Perform.” This requires that a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. You can check the BLE status at the top of the app. Perform Tutorial 5 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect to BLE as was discussed in the connect tutorial. We are also using the same notification handler as was used in the sending commands tutorial Connecting to WiFi AP Now that we are connected via BLE, paired, and have enabled notifications, we can send the command to enable the WiFi AP. Here is an outline of the steps to do so: GoProWiFiGoProBLEOpen GoPro user deviceGoProWiFiGoProBLEOpen GoPro user deviceScanningConnectedalt[If not Previously Paired]PairedReady to Communicateloop[Steps from Connect Tutorial]WiFi AP enabledAdvertisingAdvertisingConnectPair RequestPair ResponseEnable Notifications on Characteristic 1Enable Notifications on Characteristic 2Enable Notifications on Characteristic ..Enable Notifications on Characteristic NRead Wifi AP SSIDRead Wifi AP PasswordWrite to Enable WiFi APResponse sent as notificationConnect to WiFi AP Essentially we will be finding the WiFi AP information (SSID and password) via BLE, enabling the WiFi AP via BLE, then connecting to the WiFi AP. Find WiFi Information Note that the process to get this information is different than all procedures described up to this point. Whereas the previous command, setting, and query procedures all followed the Write Request-Notification Response pattern, the WiFi Information is retrieved via direct Read Requests to BLE characteristics. Get WiFi SSID The WiFi SSID can be found by reading from the WiFi AP SSID characteristic of the WiFi Access Point service. First, let’s send the read request to get the SSID (and decode it into a string). python kotlin Let’s define the attribute to read from: WIFI_AP_SSID_UUID = GOPRO_BASE_UUID.format(\"0002\") Then send the BLE read request: ssid = await client.read_gatt_char(WIFI_AP_SSID_UUID) ssid = ssid.decode() There is no need for a synchronization event as the information is available when the read_gatt_char method returns. In the demo, this information is logged as such: INFO:root:Reading the WiFi AP SSID INFO:root:SSID is GP24500456 ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_SSID.uuid).onSuccess { ssid = it.decodeToString() } Timber.i(\"SSID is $ssid\") In the demo, this information is logged as such: Getting the SSID Read characteristic b5f90002-aa8d-11e3-9046-0002a5d5c51b : value: 64:65:62:75:67:68:65:72:6F:31:31 SSID is debughero11 Get WiFi Password The WiFi password can be found by reading from the WiFi AP password characteristic of the WiFi Access Point service. First, let’s send the read request to get the password (and decode it into a string). python kotlin Let’s define the attribute to read from: WIFI_AP_PASSWORD_UUID = GOPRO_BASE_UUID.format(\"0003\") Then send the BLE read request: There is no need for a synchronization event as the information is available when the read_gatt_char method returns. In the demo, this information is logged as such: INFO:root:Reading the WiFi AP password INFO:root:Password is g@6-Tj9-C7K ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_PASSWORD.uuid).onSuccess { password = it.decodeToString() } Timber.i(\"Password is $password\") In the demo, this information is logged as such: Getting the password Read characteristic b5f90003-aa8d-11e3-9046-0002a5d5c51b : value: 7A:33:79:2D:44:43:58:2D:50:68:6A Password is z3y-DCX-Phj Enable WiFi AP Before we can connect to the WiFi AP, we have to make sure it is enabled. This is accomplished by using the “AP Control” command: Command Bytes Ap Control Enable 0x03 0x17 0x01 0x01 Ap Control Disable 0x03 0x17 0x01 0x00 This is done in the same manner that we did in the sending commands tutorial. Now, let’s write the bytes to the “Command Request UUID” to enable the WiFi AP! python kotlin event.clear() await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x03, 0x17, 0x01, 0x01])) await event.wait() Wait to receive the notification response We make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback. val enableWifiCommand = ubyteArrayOf(0x03U, 0x17U, 0x01U, 0x01U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, enableWifiCommand) receivedData.receive() Note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications. This can be seen in the demo log: python kotlin INFO:root:Enabling the WiFi AP INFO:root:Received response at handle=52: b'02:17:00' INFO:root:Command sent successfully INFO:root:WiFi AP is enabled Enabling the camera's Wifi AP Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:17:01:01 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:17:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:17:00 Command sent successfully As expected, the response was received on the correct handle and the status was “success”. Establish Connection to WiFi AP python kotlin If you have been following through the ble_enable_wifi.py script, you will notice that it ends here such that we know the WiFi SSID and password and the WiFi AP is enabled and ready to connect to. This is because there are many different methods of connecting to the WiFi AP depending on your OS and the framework you are using to develop. You could, for example, simply use your OS’s WiFi GUI to connect. While out of the scope of these tutorials, there is a programmatic example of this in the cross-platform WiFi Demo from the Open GoPro Python SDK. Using the passwsord and SSID we discovered above, we will now connect to the camera’s network: wifi.connect(ssid, password) This should show a system popup on your Android device that eventually goes away once the Wifi is connected. This connection process appears to vary drastically in time. Quiz time! 📚 ✏️ How is the WiFi password response received? A: As a read response from the WiFi AP Password characteristic B: As write responses to the WiFi Request characteristic C: As notifications of the Command Response characteristic Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is A. This (and WiFi AP SSID) is an exception to the rule. Usually responses are received as notifications to a response characteristic. However, in this case, it is received as a direct read response (since we are reading from the characteristic and not writing to it). Which of the following statements about the GoPro WiFi AP is true? A: It only needs to be enabled once and it will then always remain on B: The WiFi password will never change C: The WiFi SSID will never change D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. While the WiFi AP will remain on for some time, it can and will eventually turn off so it is always recommended to first connect via BLE and ensure that it is enabled. The password and SSID will almost never change. However, they will change if the connections are reset via Connections->Reset Connections. Troubleshooting See the first tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You are now connected to the GoPro’s Wifi AP and can send any of the HTTP commands defined in the Open GoPro Interface. Proceed to the next tutorial.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/connect-wifi#" }, { "title": "Tutorial 6: Send WiFi Commands: ", - "excerpt": "This document will provide a walk-through tutorial to send Open GoPro HTTP commands to the GoPro. It is suggested that you have first completed the Connecting to Wifi tutorial. This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future tutorial. There are two types of responses that can be received from the HTTP commands: JSON and binary. This section will deal with commands that return JSON responses. For commands with binary responses (as well as commands with JSON responses that work with the media list), see the next tutorial. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. The scripts that will be used for this tutorial can be found in the Tutorial 6 Folder. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements You must be connected to the camera via WiFi as stated in Tutorial 5. Get State You can test querying the state of your camera with HTTP over WiFi using the following script: $ python wifi_command_get_state.py See the help for parameter definitions: $ python wifi_command_get_state.py --help usage: wifi_command_get_state.py [-h] Get the state of the GoPro (status and settings). optional arguments: -h, --help show this help message and exit Preview Stream You can test enabling the UDP preview stream with HTTP over WiFi using the following script: $ python wifi_command_preview_stream.py See the help for parameter definitions: $ python wifi_command_preview_stream.py --help usage: wifi_command_preview_stream.py [-h] Enable the preview stream. optional arguments: -h, --help show this help message and exit Once enabled the stream can be viewed at udp://@:8554 (For more details see the View Stream tab in the Preview Stream section below. Load Preset Group You can test sending the load preset group command with HTTP over WiFi using the following script: $ python wifi_command_load_group.py See the help for parameter definitions: $ python wifi_command_load_group.py --help usage: wifi_command_load_group.py [-h] Load the video preset group. optional arguments: -h, --help show this help message and exit Set Shutter You can test sending the Set Shutter command with HTTP over WiFi using the following script: $ python wifi_command_set_shutter.py See the help for parameter definitions: $ python wifi_command_set_shutter.py --help usage: wifi_command_set_shutter.py [-h] Take a 3 second video. optional arguments: -h, --help show this help message and exit Set Setting You can test setting the resolution setting with HTTP over WiFi using the following script: $ python wifi_command_set_resolution.py See the help for parameter definitions: $ python wifi_command_set_resolution.py --help usage: wifi_command_set_resolution.py [-h] Set the video resolution to 1080. optional arguments: -h, --help show this help message and exit The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 6” from the dropdown and click on “Perform.” This requires: a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. You can check the BLE and Wifi statuses at the top of the app. Perform Tutorial 6 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect to The GoPro’s WiFi Access Point (AP) as was discussed in the Connecting to Wifi tutorial. Sending HTTP Commands with JSON Responses Now that we are are connected via WiFi, we can communicate via HTTP commands. python kotlin We will use the requests package to send the various HTTP commands. We are building the endpoints using the GOPRO_BASE_URL defined in the tutorial package’s __init__.py We are using ktor for the HTTP client. We are using an abstracted get function from our Wifi class to send get requests as such: private val client by lazy { HttpClient(CIO) { install(HttpTimeout) } } suspend fun get(endpoint: String, timeoutMs: Long = 5000L): JsonObject { Timber.d(\"GET request to: $endpoint\") val response = client.request(endpoint) { timeout { requestTimeoutMillis = timeoutMs } } val bodyAsString: String = response.body() return prettyJson.parseToJsonElement(bodyAsString).jsonObject } Both Command Requests and Setting Requests follow the same procedure: Send HTTP GET command to appropriate endpoint Receive confirmation from GoPro (via HTTP response) that request was received. GoPro reacts to command The HTTP response only indicates that the request was received correctly. The relevant behavior of the GoPro must be observed to verify when the command’s effects have been applied. Open GoPro user deviceGoProPC connected to WiFi APCommand Request (GET)Command Response (HTTP 200 OK)Apply affects of command when ableOpen GoPro user deviceGoPro Get State The first command we will be sending is Get State. This command will return all of the current settings and values. It is basically a combination of the Get All Settings and Get All Statuses commands that were sent via BLE. Since there is no way to query individual settings / statuses via WiFi (or register for asynchronous notifications when they change), this is the only option to query setting / status information via WiFi. The command writes to the following endpoint: /gopro/camera/state Let’s build the endpoint then send the GET request and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + \"/gopro/camera/state\" response = requests.get(url) response.raise_for_status() var response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/state\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") The response will log as such (abbreviated for brevity): INFO:root:Getting GoPro's status and settings: sending http://10.5.5.9:8080/gopro/camera/state INFO:root:Command sent successfully INFO:root:Response: { \"status\": { \"1\": 1, \"2\": 2, \"3\": 0, \"4\": 255, \"6\": 0, \"8\": 0, \"9\": 0, \"10\": 0, \"11\": 0, \"13\": 0, \"14\": 0, \"17\": 1, ... \"settings\": { \"2\": 9, \"3\": 1, \"5\": 0, \"6\": 1, \"13\": 1, \"19\": 0, \"24\": 0, \"30\": 0, \"31\": 0, \"32\": 10, \"41\": 9, \"42\": 5, Timber.i(prettyJson.encodeToString(response)) The response will log as such (abbreviated for brevity): Getting camera state GET request to: http://10.5.5.9:8080/gopro/camera/state { \"status\": { \"1\": 1, \"2\": 4, \"3\": 0, \"4\": 255, \"6\": 0, \"8\": 0, \"9\": 0, \"10\": 0, \"11\": 0, \"13\": 0, ... \"113\": 0, \"114\": 0, \"115\": 0, \"116\": 0, \"117\": 31154688 }, \"settings\": { \"2\": 9, \"3\": 1, \"5\": 0, \"6\": 1, \"13\": 1, ... \"177\": 0, \"178\": 1, \"179\": 3, \"180\": 0, \"181\": 0 } } We can see what each of these values mean by looking at the Open GoPro Interface. For example (for settings): ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 Load Preset Group The next command we will be sending is Load Preset Group, which is used to toggle between the 3 groups of presets (video, photo, and timelapse). The preset groups ID’s are: Command Bytes Load Video Preset Group 1000 Load Photo Preset Group 1001 Load Timelapse Preset Group 1002 It is possible that the preset GroupID values will vary in future cameras. The only absolutely correct way to know the preset ID is to read them from the “Get Preset Status” protobuf command. A future lab will discuss protobuf commands. python kotlin url = GOPRO_BASE_URL + \"/gopro/camera/presets/set_group?id=1000\" response = requests.get(url) response.raise_for_status() response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/presets/load?id=1000\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") This will log as such: INFO:root:Loading the video preset group: sending http://10.5.5.9:8080/gopro/camera/presets/set_group?id=1000 INFO:root:Command sent successfully INFO:root:Response: {} Timber.i(prettyJson.encodeToString(response)) The response will log as such: Loading Video Preset Group GET request to: http://10.5.5.9:8080/gopro/camera/presets/load?id=1000 { } Lastly, we print the response’s JSON data: The response JSON is empty. This is expected in the case of a success. You should hear the camera beep and switch to the Cinematic Preset (assuming it wasn’t already set). You can verify this by seeing the preset name in the pill at bottom middle of the screen. Load Preset Set Shutter The next command we will be sending is Set Shutter. which is used to start and stop encoding. python kotlin url = GOPRO_BASE_URL + f\"/gopro/camera/shutter/start\" response = requests.get(url) response.raise_for_status() response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/shutter/start\") Lastly, we print the response’s JSON data: This command does not return a JSON response so we don’t print the response This will log as such: python kotlin INFO:root:Turning the shutter on: sending http://10.5.5.9:8080/gopro/camera/shutter/start INFO:root:Command sent successfully Timber.i(prettyJson.encodeToString(response)) The response will log as such: Setting Shutter On GET request to: http://10.5.5.9:8080/gopro/camera/shutter/start { } We can then wait a few seconds and repeat the above procedure to set the shutter off using gopro/camera/shutter/stop. The shutter can not be set on if the camera is encoding or set off if the camera is not encoding. An attempt to do so will result in an error response. Set Setting The next command will be sending is Set Setting. This end point is used to update all of the settings on the camera. It is analogous to BLE commands like Set Video Resolution. It is important to note that many settings are dependent on the video resolution (and other settings). For example, certain FPS values are not valid with certain resolutions. In general, higher resolutions only allow lower FPS values. Check the camera capabilities to see which settings are valid for given use cases. Let’s build the endpoint first to set the Video Resolution to 1080 (the setting_id and option value comes from the command table linked above). python kotlin url = GOPRO_BASE_URL + f\"/gopro/camera/setting?setting=2&option=9\" response = requests.get(url) response.raise_for_status() response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/setting?setting=2&option=9\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") This will log as such: INFO:root:Setting the video resolution to 1080: sending http://10.5.5.9:8080/gopro/camera/setting?setting_id=2&opt_value=9 INFO:root:Command sent successfully INFO:root:Response: {} Timber.i(prettyJson.encodeToString(response)) The response will log as such: Setting Resolution to 1080 GET request to: http://10.5.5.9:8080/gopro/camera/setting?setting=2&option=9 { } The response JSON is empty. This is expected in the case of a success. You should hear the camera beep and see the video resolution change to 1080 in the pill in the bottom-middle of the screen: Video Resolution As a reader exercise, try using the [Get State] command to verify that the resolution has changed. Preview Stream The next command we will be sending is Preview Stream. This command will enable (or disable) the preview stream . It is then possible to view the preview stream from a media player. The commands write to the following endpoints: Command Endpoint start preview stream /gopro/camera/stream/start stop preview stream /gopro/camera/stream/stop Let’s build the endpoint then send the GET request and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + \"/gopro/camera/stream/start\" response = requests.get(url) response.raise_for_status() TODO Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") This will log as such: INFO:root:Starting the preview stream: sending http://10.5.5.9:8080/gopro/camera/stream/start INFO:root:Command sent successfully INFO:root:Response: {} TODO The response JSON is empty. This is expected in the case of a success. Once enabled, the stream can be viewed at udp://@:8554. Here is an example of viewing this using VLC: The screen may slightly vary depending on your OS Select Media–>Open Network Stream Enter the path as such: Configure Preview Stream Select play The preview stream should now be visible. Quiz time! 📚 ✏️ What is the significance of empty JSON in an HTTP response? A: Always an error! The command was not received correctly. B: If the status is ok (200), this is expected. C: This is expected for errors (code other than 200) but not expected for ok (200). Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. It is common for the JSON response to be empty if the command was received successfully but there is no additional information to return at the current time. Which of the of the following is not a real preset group? A: Timelapse B: Photo C: Burst D: Video Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There are 3 preset groups (Timelapse, Photo, and Video). These can be set via the Load Preset Group command. How do you query the current video resolution setting (id = 2) via WiFi? A: Send GET to /gopro/camera/state?setting_id=2 B: Send GET to /gopro/camera/state?get_setting=2 C: Send POST to /gopro/camera/state with request &8216;setting_id=2&8217; D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. You can&8217;t query individual settings or statuses with the HTTP API. In order to get the value of a specific setting you&8217;ll need to send a GET to /gopro/camera/state and parse the value of the desired setting from the JSON response. Troubleshooting HTTP Logging Wireshark can be used to view the HTTP commands and responses between the PC and the GoPro. Start a Wireshark capture on the WiFi adapter that is used to connect to the GoPro Filter for the GoPro IP address (10.5.5.9) Wireshark Good Job! Congratulations 🤙 You can now send any of the HTTP commands defined in the Open GoPro Interface that return JSON responses. You may have noted that we did not discuss one of these (Get Media List) in this tutorial. Proceed to the next tutorial to see how to get and perform operations using the media list.", + "excerpt": "This document will provide a walk-through tutorial to send Open GoPro HTTP commands to the GoPro. It is suggested that you have first completed the Connecting to Wifi tutorial. This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future tutorial. There are two types of responses that can be received from the HTTP commands: JSON and binary. This section will deal with commands that return JSON responses. For commands with binary responses (as well as commands with JSON responses that work with the media list), see the next tutorial. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. The scripts that will be used for this tutorial can be found in the Tutorial 6 Folder. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements You must be connected to the camera via WiFi as stated in Tutorial 5. Get State You can test querying the state of your camera with HTTP over WiFi using the following script: $ python wifi_command_get_state.py See the help for parameter definitions: $ python wifi_command_get_state.py --help usage: wifi_command_get_state.py [-h] Get the state of the GoPro (status and settings). optional arguments: -h, --help show this help message and exit Preview Stream You can test enabling the UDP preview stream with HTTP over WiFi using the following script: $ python wifi_command_preview_stream.py See the help for parameter definitions: $ python wifi_command_preview_stream.py --help usage: wifi_command_preview_stream.py [-h] Enable the preview stream. optional arguments: -h, --help show this help message and exit Once enabled the stream can be viewed at udp://@:8554 (For more details see the View Stream tab in the Preview Stream section below. Load Preset Group You can test sending the load preset group command with HTTP over WiFi using the following script: $ python wifi_command_load_group.py See the help for parameter definitions: $ python wifi_command_load_group.py --help usage: wifi_command_load_group.py [-h] Load the video preset group. optional arguments: -h, --help show this help message and exit Set Shutter You can test sending the Set Shutter command with HTTP over WiFi using the following script: $ python wifi_command_set_shutter.py See the help for parameter definitions: $ python wifi_command_set_shutter.py --help usage: wifi_command_set_shutter.py [-h] Take a 3 second video. optional arguments: -h, --help show this help message and exit Set Setting You can test setting the resolution setting with HTTP over WiFi using the following script: $ python wifi_command_set_resolution.py See the help for parameter definitions: $ python wifi_command_set_resolution.py --help usage: wifi_command_set_resolution.py [-h] Set the video resolution to 1080. optional arguments: -h, --help show this help message and exit The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 6” from the dropdown and click on “Perform.” This requires: a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. You can check the BLE and Wifi statuses at the top of the app. Perform Tutorial 6 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect to The GoPro’s WiFi Access Point (AP) as was discussed in the Connecting to Wifi tutorial. Sending HTTP Commands with JSON Responses Now that we are are connected via WiFi, we can communicate via HTTP commands. python kotlin We will use the requests package to send the various HTTP commands. We are building the endpoints using the GOPRO_BASE_URL defined in the tutorial package’s __init__.py We are using ktor for the HTTP client. We are using an abstracted get function from our Wifi class to send get requests as such: private val client by lazy { HttpClient(CIO) { install(HttpTimeout) } } suspend fun get(endpoint: String, timeoutMs: Long = 5000L): JsonObject { Timber.d(\"GET request to: $endpoint\") val response = client.request(endpoint) { timeout { requestTimeoutMillis = timeoutMs } } val bodyAsString: String = response.body() return prettyJson.parseToJsonElement(bodyAsString).jsonObject } Both Command Requests and Setting Requests follow the same procedure: Send HTTP GET command to appropriate endpoint Receive confirmation from GoPro (via HTTP response) that request was received. GoPro reacts to command The HTTP response only indicates that the request was received correctly. The relevant behavior of the GoPro must be observed to verify when the command’s effects have been applied. GoProOpen GoPro user deviceGoProOpen GoPro user devicePC connected to WiFi APCommand Request (GET)Command Response (HTTP 200 OK)Apply affects of command when able Get State The first command we will be sending is Get State. This command will return all of the current settings and values. It is basically a combination of the Get All Settings and Get All Statuses commands that were sent via BLE. Since there is no way to query individual settings / statuses via WiFi (or register for asynchronous notifications when they change), this is the only option to query setting / status information via WiFi. The command writes to the following endpoint: /gopro/camera/state Let’s build the endpoint then send the GET request and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + \"/gopro/camera/state\" response = requests.get(url) response.raise_for_status() var response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/state\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") The response will log as such (abbreviated for brevity): INFO:root:Getting GoPro's status and settings: sending http://10.5.5.9:8080/gopro/camera/state INFO:root:Command sent successfully INFO:root:Response: { \"status\": { \"1\": 1, \"2\": 2, \"3\": 0, \"4\": 255, \"6\": 0, \"8\": 0, \"9\": 0, \"10\": 0, \"11\": 0, \"13\": 0, \"14\": 0, \"17\": 1, ... \"settings\": { \"2\": 9, \"3\": 1, \"5\": 0, \"6\": 1, \"13\": 1, \"19\": 0, \"24\": 0, \"30\": 0, \"31\": 0, \"32\": 10, \"41\": 9, \"42\": 5, Timber.i(prettyJson.encodeToString(response)) The response will log as such (abbreviated for brevity): Getting camera state GET request to: http://10.5.5.9:8080/gopro/camera/state { \"status\": { \"1\": 1, \"2\": 4, \"3\": 0, \"4\": 255, \"6\": 0, \"8\": 0, \"9\": 0, \"10\": 0, \"11\": 0, \"13\": 0, ... \"113\": 0, \"114\": 0, \"115\": 0, \"116\": 0, \"117\": 31154688 }, \"settings\": { \"2\": 9, \"3\": 1, \"5\": 0, \"6\": 1, \"13\": 1, ... \"177\": 0, \"178\": 1, \"179\": 3, \"180\": 0, \"181\": 0 } } We can see what each of these values mean by looking at the Open GoPro Interface. For example (for settings): ID 2 == 9 equates to Resolution == 1080 ID 3 == 1 equates to FPS == 120 Load Preset Group The next command we will be sending is Load Preset Group, which is used to toggle between the 3 groups of presets (video, photo, and timelapse). The preset groups ID’s are: Command Bytes Load Video Preset Group 1000 Load Photo Preset Group 1001 Load Timelapse Preset Group 1002 It is possible that the preset GroupID values will vary in future cameras. The only absolutely correct way to know the preset ID is to read them from the “Get Preset Status” protobuf command. A future lab will discuss protobuf commands. python kotlin url = GOPRO_BASE_URL + \"/gopro/camera/presets/set_group?id=1000\" response = requests.get(url) response.raise_for_status() response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/presets/load?id=1000\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") This will log as such: INFO:root:Loading the video preset group: sending http://10.5.5.9:8080/gopro/camera/presets/set_group?id=1000 INFO:root:Command sent successfully INFO:root:Response: {} Timber.i(prettyJson.encodeToString(response)) The response will log as such: Loading Video Preset Group GET request to: http://10.5.5.9:8080/gopro/camera/presets/load?id=1000 { } Lastly, we print the response’s JSON data: The response JSON is empty. This is expected in the case of a success. You should hear the camera beep and switch to the Cinematic Preset (assuming it wasn’t already set). You can verify this by seeing the preset name in the pill at bottom middle of the screen. Load Preset Set Shutter The next command we will be sending is Set Shutter. which is used to start and stop encoding. python kotlin url = GOPRO_BASE_URL + f\"/gopro/camera/shutter/start\" response = requests.get(url) response.raise_for_status() response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/shutter/start\") Lastly, we print the response’s JSON data: This command does not return a JSON response so we don’t print the response This will log as such: python kotlin INFO:root:Turning the shutter on: sending http://10.5.5.9:8080/gopro/camera/shutter/start INFO:root:Command sent successfully Timber.i(prettyJson.encodeToString(response)) The response will log as such: Setting Shutter On GET request to: http://10.5.5.9:8080/gopro/camera/shutter/start { } We can then wait a few seconds and repeat the above procedure to set the shutter off using gopro/camera/shutter/stop. The shutter can not be set on if the camera is encoding or set off if the camera is not encoding. An attempt to do so will result in an error response. Set Setting The next command will be sending is Set Setting. This end point is used to update all of the settings on the camera. It is analogous to BLE commands like Set Video Resolution. It is important to note that many settings are dependent on the video resolution (and other settings). For example, certain FPS values are not valid with certain resolutions. In general, higher resolutions only allow lower FPS values. Check the camera capabilities to see which settings are valid for given use cases. Let’s build the endpoint first to set the Video Resolution to 1080 (the setting_id and option value comes from the command table linked above). python kotlin url = GOPRO_BASE_URL + f\"/gopro/camera/setting?setting=2&option=9\" response = requests.get(url) response.raise_for_status() response = wifi.get(GOPRO_BASE_URL + \"gopro/camera/setting?setting=2&option=9\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") This will log as such: INFO:root:Setting the video resolution to 1080: sending http://10.5.5.9:8080/gopro/camera/setting?setting_id=2&opt_value=9 INFO:root:Command sent successfully INFO:root:Response: {} Timber.i(prettyJson.encodeToString(response)) The response will log as such: Setting Resolution to 1080 GET request to: http://10.5.5.9:8080/gopro/camera/setting?setting=2&option=9 { } The response JSON is empty. This is expected in the case of a success. You should hear the camera beep and see the video resolution change to 1080 in the pill in the bottom-middle of the screen: Video Resolution As a reader exercise, try using the [Get State] command to verify that the resolution has changed. Preview Stream The next command we will be sending is Preview Stream. This command will enable (or disable) the preview stream . It is then possible to view the preview stream from a media player. The commands write to the following endpoints: Command Endpoint start preview stream /gopro/camera/stream/start stop preview stream /gopro/camera/stream/stop Let’s build the endpoint then send the GET request and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + \"/gopro/camera/stream/start\" response = requests.get(url) response.raise_for_status() TODO Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") This will log as such: INFO:root:Starting the preview stream: sending http://10.5.5.9:8080/gopro/camera/stream/start INFO:root:Command sent successfully INFO:root:Response: {} TODO The response JSON is empty. This is expected in the case of a success. Once enabled, the stream can be viewed at udp://@:8554. Here is an example of viewing this using VLC: The screen may slightly vary depending on your OS Select Media–>Open Network Stream Enter the path as such: Configure Preview Stream Select play The preview stream should now be visible. Quiz time! 📚 ✏️ What is the significance of empty JSON in an HTTP response? A: Always an error! The command was not received correctly. B: If the status is ok (200), this is expected. C: This is expected for errors (code other than 200) but not expected for ok (200). Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is B. It is common for the JSON response to be empty if the command was received successfully but there is no additional information to return at the current time. Which of the of the following is not a real preset group? A: Timelapse B: Photo C: Burst D: Video Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is C. There are 3 preset groups (Timelapse, Photo, and Video). These can be set via the Load Preset Group command. How do you query the current video resolution setting (id = 2) via WiFi? A: Send GET to /gopro/camera/state?setting_id=2 B: Send GET to /gopro/camera/state?get_setting=2 C: Send POST to /gopro/camera/state with request &8216;setting_id=2&8217; D: None of the Above Submit Answer Correct!! 😃 Incorrect!! 😭 The correct answer is D. You can&8217;t query individual settings or statuses with the HTTP API. In order to get the value of a specific setting you&8217;ll need to send a GET to /gopro/camera/state and parse the value of the desired setting from the JSON response. Troubleshooting HTTP Logging Wireshark can be used to view the HTTP commands and responses between the PC and the GoPro. Start a Wireshark capture on the WiFi adapter that is used to connect to the GoPro Filter for the GoPro IP address (10.5.5.9) Wireshark Good Job! Congratulations 🤙 You can now send any of the HTTP commands defined in the Open GoPro Interface that return JSON responses. You may have noted that we did not discuss one of these (Get Media List) in this tutorial. Proceed to the next tutorial to see how to get and perform operations using the media list.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/send-wifi-commands#" }, { "title": "Tutorial 7: Camera Media List: ", - "excerpt": "This document will provide a walk-through tutorial to send Open GoPro HTTP commands to the GoPro, specifically to get the media list and perform operations on it (downloading pictures, videos, etc.) It is suggested that you have first completed the Connecting to Wifi and Sending WiFi Commands tutorials. This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future tutorial. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. The scripts that will be used for this tutorial can be found in the Tutorial 7 Folder. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements You must be connected to the camera via WiFi in order to run these scripts. You can do this by manually to the SSID and password listed on your camera or by leaving the Establish Connection to WiFi AP script from Tutorial 5 running in the background. Download Media File You can downloading a file from your camera with HTTP over WiFi using the following script: $ python wifi_media_download_file.py See the help for parameter definitions: $ python wifi_media_download_file.py --help usage: wifi_media_download_file.py [-h] Find a photo on the camera and download it to the computer. optional arguments: -h, --help show this help message and exit Get Media Thumbnail You can downloading the thumbnail for a media file from your camera with HTTP over WiFi using the following script: $ python wifi_media_get_thumbnail.py See the help for parameter definitions: $ python wifi_media_get_thumbnail.py --help usage: wifi_media_get_thumbnail.py [-h] Get the thumbnail for a media file. optional arguments: -h, --help show this help message and exit The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 7” from the dropdown and click on “Perform.” This requires: a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. You can check the BLE and Wifi statuses at the top of the app. Perform Tutorial 7 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect to The GoPro’s WiFi Access Point (AP) as was discussed in the Connecting to Wifi tutorial. Get Media List Now that we are are connected via WiFi, we will get the media list using the same procedure to send HTTP commands as in the previous tutorial. We get the media list via the Get Media List command. This command will return a JSON structure of all of the media files (pictures, videos) on the camera with corresponding information about each media file. Let’s build the endpoint, send the GET request, and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + \"/gopro/media/list\" response = requests.get(url) response.raise_for_status() val response = wifi.get(GOPRO_BASE_URL + \"gopro/media/list\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") The response will log as such (abbreviated for brevity): INFO:root:Getting the media list: sending http://10.5.5.9:8080/gopro/media/list INFO:root:Command sent successfully INFO:root:Response: { \"id\": \"2510746051348624995\", \"media\": [ { \"d\": \"100GOPRO\", \"fs\": [ { \"n\": \"GOPR0987.JPG\", \"cre\": \"1618583762\", \"mod\": \"1618583762\", \"s\": \"5013927\" }, { \"n\": \"GOPR0988.JPG\", \"cre\": \"1618583764\", \"mod\": \"1618583764\", \"s\": \"5009491\" }, { \"n\": \"GOPR0989.JPG\", \"cre\": \"1618583766\", \"mod\": \"1618583766\", \"s\": \"5031861\" }, { \"n\": \"GX010990.MP4\", \"cre\": \"1451608343\", \"mod\": \"1451608343\", \"glrv\": \"806586\", \"ls\": \"-1\", \"s\": \"10725219\" }, Timber.i(\"Files in media list: ${prettyJson.encodeToString(fileList)}\") The response will log as such (abbreviated for brevity): GET request to: http://10.5.5.9:8080/gopro/media/list Complete media list: { \"id\": \"4386457835676877283\", \"media\": [ { \"d\": \"100GOPRO\", \"fs\": [ { \"n\": \"GOPR0232.JPG\", \"cre\": \"1748997965\", \"mod\": \"1748997965\", \"s\": \"7618898\" }, { \"n\": \"GOPR0233.JPG\", \"cre\": \"1748998273\", \"mod\": \"1748998273\", \"s\": \"7653472\" }, ... { \"n\": \"GX010259.MP4\", \"cre\": \"1677828860\", \"mod\": \"1677828860\", \"glrv\": \"943295\", \"ls\": \"-1\", \"s\": \"9788009\" } ] } ] } The media list format is defined in the Open GoPro Specification. We won’t be rehashing that here but will provide examples below of using the media list. One common functionality is to get the list of media file names, which can be done as such: python kotlin print([x[\"n\"] for x in media_list[\"media\"][0][\"fs\"]]) That is, access the list at the fs tag at the first element of the media tag, then make a list of all of the names (n tag of each element) in the fs list. val fileList = response[\"media\"]?.jsonArray?.first()?.jsonObject?.get(\"fs\")?.jsonArray?.map { mediaEntry -> mediaEntry.jsonObject[\"n\"] }?.map { it.toString().replace(\"\\\"\", \"\") } That is: Access the JSON array at the fs tag at the first element of the media tag Make a list of all of the names (n tag of each element) in the fs list. Map this list to string and remove backslashes 3. Media List Operations Whereas all of the WiFi commands described until now have returned JSON responses, most of the media list operations return binary data. From an HTTP perspective, the behavior is the same. However, the GET response will contain a large binary chunk of information so we will loop through it with the requests library as such, writing up to 8 kB at a time: diskOpen GoPro user deviceGoProPC connected to WiFi APloop[write until complete]Get Media List (GET)Media List (HTTP 200 OK)Command Request (GET)Binary Response (HTTP 200 OK)write <= 8KdiskOpen GoPro user deviceGoPro Download Media File The next command we will be sending is Download Media. Specifically, we will be downloading a photo. The camera must have at least one photo in its media list in order for this to work. First, we get the media list as in Get Media List . Then we search through the list of file names in the media list looking for a photo (i.e. a file whose name ends in .jpg). Once we find a photo, we proceed: python kotlin media_list = get_media_list() photo: Optional[str] = None for media_file in [x[\"n\"] for x in media_list[\"media\"][0][\"fs\"]]: if media_file.lower().endswith(\".jpg\"): logger.info(f\"found a photo: {media_file}\") photo = media_file break val photo = fileList?.firstOrNull { it.endsWith(ignoreCase = true, suffix = \"jpg\") } ?: throw Exception(\"Not able to find a .jpg in the media list\") Timber.i(\"Found a photo: $photo\") Now let’s build the endpoint, send the GET request, and check the response for errors. Any errors will raise an exception. The endpoint will start with “videos” for both photos and videos python kotlin url = GOPRO_BASE_URL + f\"videos/DCIM/100GOPRO/{photo}\" with requests.get(url, stream=True) as request: request.raise_for_status() Lastly, we iterate through the binary content in 8 kB chunks, writing to a local file: file = photo.split(\".\")[0] + \".jpg\" with open(file, \"wb\") as f: logger.info(f\"receiving binary stream to {file}...\") for chunk in request.iter_content(chunk_size=8192): f.write(chunk) return wifi.getFile( GOPRO_BASE_URL + \"videos/DCIM/100GOPRO/$photo\", appContainer.applicationContext ) TODO FIX THIS This will log as such: python kotlin INFO:root:found a photo: GOPR0987.JPG INFO:root:Downloading GOPR0987.JPG INFO:root:Sending: http://10.5.5.9:8080/videos/DCIM/100GOPRO/GOPR0987.JPG INFO:root:receiving binary stream to GOPR0987.jpg... Once complete, the GOPR0987_thumbnail.jpg file will be available from where the demo script was called. Found a photo: GOPR0232.JPG Downloading photo: GOPR0232.JPG... Once complete, the photo will display in the tutorial window. Get Media Thumbnail The next command we will be sending is Get Media thumbnail . Specifically, we will be getting the thumbnail for a photo. The camera must have at least one photo in its media list in order for this to work. There is a separate commandto get a media “screennail” First, we get the media list as in Get Media List . Then we search through the list of file names in the media list looking for a photo (i.e. a file whose name ends in .jpg). Once we find a photo, we proceed: python kotlin media_list = get_media_list() photo: Optional[str] = None for media_file in [x[\"n\"] for x in media_list[\"media\"][0][\"fs\"]]: if media_file.lower().endswith(\".jpg\"): logger.info(f\"found a photo: {media_file}\") photo = media_file break TODO Now let’s build the endpoint, send the GET request, and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + f\"/gopro/media/thumbnail?path=100GOPRO/{photo}\" with requests.get(url, stream=True) as request: request.raise_for_status() Lastly, we iterate through the binary content in 8 kB chunks, writing to a local file: file = photo.split(\".\")[0] + \".jpg\" with open(file, \"wb\") as f: logger.info(f\"receiving binary stream to {file}...\") for chunk in request.iter_content(chunk_size=8192): f.write(chunk) TODO This will log as such: python kotlin INFO:root:found a photo: GOPR0987.JPG INFO:root:Getting the thumbnail for GOPR0987.JPG INFO:root:Sending: http://10.5.5.9:8080/gopro/media/thumbnail?path=100GOPRO/GOPR0987.JPG INFO:root:receiving binary stream to GOPR0987_thumbnail.jpg... TODO Troubleshooting See the previous tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now query the GoPro’s media list and retrieve binary information for media file. This is currently last tutorial. Stay tuned for more 👍 At this point you should be able to start creating a useful example using the Open GoPro Interface. For some inspiration check out some of the demos.", + "excerpt": "This document will provide a walk-through tutorial to send Open GoPro HTTP commands to the GoPro, specifically to get the media list and perform operations on it (downloading pictures, videos, etc.) It is suggested that you have first completed the Connecting to Wifi and Sending WiFi Commands tutorials. This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future tutorial. Requirements It is assumed that the hardware and software requirements from the connect tutorial are present and configured correctly. The scripts that will be used for this tutorial can be found in the Tutorial 7 Folder. Just Show me the Demo(s)!! python kotlin Each of the scripts for this tutorial can be found in the Tutorial 2 directory. Python >= 3.8.x must be used as specified in the requirements You must be connected to the camera via WiFi in order to run these scripts. You can do this by manually to the SSID and password listed on your camera or by leaving the Establish Connection to WiFi AP script from Tutorial 5 running in the background. Download Media File You can downloading a file from your camera with HTTP over WiFi using the following script: $ python wifi_media_download_file.py See the help for parameter definitions: $ python wifi_media_download_file.py --help usage: wifi_media_download_file.py [-h] Find a photo on the camera and download it to the computer. optional arguments: -h, --help show this help message and exit Get Media Thumbnail You can downloading the thumbnail for a media file from your camera with HTTP over WiFi using the following script: $ python wifi_media_get_thumbnail.py See the help for parameter definitions: $ python wifi_media_get_thumbnail.py --help usage: wifi_media_get_thumbnail.py [-h] Get the thumbnail for a media file. optional arguments: -h, --help show this help message and exit The Kotlin file for this tutorial can be found on Github. To perform the tutorial, run the Android Studio project, select “Tutorial 7” from the dropdown and click on “Perform.” This requires: a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. You can check the BLE and Wifi statuses at the top of the app. Perform Tutorial 7 This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click “Exit Tutorial” to return to the Tutorial selection screen. Setup We must first connect to The GoPro’s WiFi Access Point (AP) as was discussed in the Connecting to Wifi tutorial. Get Media List Now that we are are connected via WiFi, we will get the media list using the same procedure to send HTTP commands as in the previous tutorial. We get the media list via the Get Media List command. This command will return a JSON structure of all of the media files (pictures, videos) on the camera with corresponding information about each media file. Let’s build the endpoint, send the GET request, and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + \"/gopro/media/list\" response = requests.get(url) response.raise_for_status() val response = wifi.get(GOPRO_BASE_URL + \"gopro/media/list\") Lastly, we print the response’s JSON data: python kotlin logger.info(f\"Response: {json.dumps(response.json(), indent=4)}\") The response will log as such (abbreviated for brevity): INFO:root:Getting the media list: sending http://10.5.5.9:8080/gopro/media/list INFO:root:Command sent successfully INFO:root:Response: { \"id\": \"2510746051348624995\", \"media\": [ { \"d\": \"100GOPRO\", \"fs\": [ { \"n\": \"GOPR0987.JPG\", \"cre\": \"1618583762\", \"mod\": \"1618583762\", \"s\": \"5013927\" }, { \"n\": \"GOPR0988.JPG\", \"cre\": \"1618583764\", \"mod\": \"1618583764\", \"s\": \"5009491\" }, { \"n\": \"GOPR0989.JPG\", \"cre\": \"1618583766\", \"mod\": \"1618583766\", \"s\": \"5031861\" }, { \"n\": \"GX010990.MP4\", \"cre\": \"1451608343\", \"mod\": \"1451608343\", \"glrv\": \"806586\", \"ls\": \"-1\", \"s\": \"10725219\" }, Timber.i(\"Files in media list: ${prettyJson.encodeToString(fileList)}\") The response will log as such (abbreviated for brevity): GET request to: http://10.5.5.9:8080/gopro/media/list Complete media list: { \"id\": \"4386457835676877283\", \"media\": [ { \"d\": \"100GOPRO\", \"fs\": [ { \"n\": \"GOPR0232.JPG\", \"cre\": \"1748997965\", \"mod\": \"1748997965\", \"s\": \"7618898\" }, { \"n\": \"GOPR0233.JPG\", \"cre\": \"1748998273\", \"mod\": \"1748998273\", \"s\": \"7653472\" }, ... { \"n\": \"GX010259.MP4\", \"cre\": \"1677828860\", \"mod\": \"1677828860\", \"glrv\": \"943295\", \"ls\": \"-1\", \"s\": \"9788009\" } ] } ] } The media list format is defined in the Open GoPro Specification. We won’t be rehashing that here but will provide examples below of using the media list. One common functionality is to get the list of media file names, which can be done as such: python kotlin print([x[\"n\"] for x in media_list[\"media\"][0][\"fs\"]]) That is, access the list at the fs tag at the first element of the media tag, then make a list of all of the names (n tag of each element) in the fs list. val fileList = response[\"media\"]?.jsonArray?.first()?.jsonObject?.get(\"fs\")?.jsonArray?.map { mediaEntry -> mediaEntry.jsonObject[\"n\"] }?.map { it.toString().replace(\"\\\"\", \"\") } That is: Access the JSON array at the fs tag at the first element of the media tag Make a list of all of the names (n tag of each element) in the fs list. Map this list to string and remove backslashes 3. Media List Operations Whereas all of the WiFi commands described until now have returned JSON responses, most of the media list operations return binary data. From an HTTP perspective, the behavior is the same. However, the GET response will contain a large binary chunk of information so we will loop through it with the requests library as such, writing up to 8 kB at a time: GoProOpen GoPro user devicediskGoProOpen GoPro user devicediskPC connected to WiFi APloop[write until complete]Get Media List (GET)Media List (HTTP 200 OK)Command Request (GET)Binary Response (HTTP 200 OK)write <= 8K Download Media File The next command we will be sending is Download Media. Specifically, we will be downloading a photo. The camera must have at least one photo in its media list in order for this to work. First, we get the media list as in Get Media List . Then we search through the list of file names in the media list looking for a photo (i.e. a file whose name ends in .jpg). Once we find a photo, we proceed: python kotlin media_list = get_media_list() photo: Optional[str] = None for media_file in [x[\"n\"] for x in media_list[\"media\"][0][\"fs\"]]: if media_file.lower().endswith(\".jpg\"): logger.info(f\"found a photo: {media_file}\") photo = media_file break val photo = fileList?.firstOrNull { it.endsWith(ignoreCase = true, suffix = \"jpg\") } ?: throw Exception(\"Not able to find a .jpg in the media list\") Timber.i(\"Found a photo: $photo\") Now let’s build the endpoint, send the GET request, and check the response for errors. Any errors will raise an exception. The endpoint will start with “videos” for both photos and videos python kotlin url = GOPRO_BASE_URL + f\"videos/DCIM/100GOPRO/{photo}\" with requests.get(url, stream=True) as request: request.raise_for_status() Lastly, we iterate through the binary content in 8 kB chunks, writing to a local file: file = photo.split(\".\")[0] + \".jpg\" with open(file, \"wb\") as f: logger.info(f\"receiving binary stream to {file}...\") for chunk in request.iter_content(chunk_size=8192): f.write(chunk) return wifi.getFile( GOPRO_BASE_URL + \"videos/DCIM/100GOPRO/$photo\", appContainer.applicationContext ) TODO FIX THIS This will log as such: python kotlin INFO:root:found a photo: GOPR0987.JPG INFO:root:Downloading GOPR0987.JPG INFO:root:Sending: http://10.5.5.9:8080/videos/DCIM/100GOPRO/GOPR0987.JPG INFO:root:receiving binary stream to GOPR0987.jpg... Once complete, the GOPR0987_thumbnail.jpg file will be available from where the demo script was called. Found a photo: GOPR0232.JPG Downloading photo: GOPR0232.JPG... Once complete, the photo will display in the tutorial window. Get Media Thumbnail The next command we will be sending is Get Media thumbnail . Specifically, we will be getting the thumbnail for a photo. The camera must have at least one photo in its media list in order for this to work. There is a separate commandto get a media “screennail” First, we get the media list as in Get Media List . Then we search through the list of file names in the media list looking for a photo (i.e. a file whose name ends in .jpg). Once we find a photo, we proceed: python kotlin media_list = get_media_list() photo: Optional[str] = None for media_file in [x[\"n\"] for x in media_list[\"media\"][0][\"fs\"]]: if media_file.lower().endswith(\".jpg\"): logger.info(f\"found a photo: {media_file}\") photo = media_file break TODO Now let’s build the endpoint, send the GET request, and check the response for errors. Any errors will raise an exception. python kotlin url = GOPRO_BASE_URL + f\"/gopro/media/thumbnail?path=100GOPRO/{photo}\" with requests.get(url, stream=True) as request: request.raise_for_status() Lastly, we iterate through the binary content in 8 kB chunks, writing to a local file: file = photo.split(\".\")[0] + \".jpg\" with open(file, \"wb\") as f: logger.info(f\"receiving binary stream to {file}...\") for chunk in request.iter_content(chunk_size=8192): f.write(chunk) TODO This will log as such: python kotlin INFO:root:found a photo: GOPR0987.JPG INFO:root:Getting the thumbnail for GOPR0987.JPG INFO:root:Sending: http://10.5.5.9:8080/gopro/media/thumbnail?path=100GOPRO/GOPR0987.JPG INFO:root:receiving binary stream to GOPR0987_thumbnail.jpg... TODO Troubleshooting See the previous tutorial’s troubleshooting section. Good Job! Congratulations 🤙 You can now query the GoPro’s media list and retrieve binary information for media file. This is currently last tutorial. Stay tuned for more 👍 At this point you should be able to start creating a useful example using the Open GoPro Interface. For some inspiration check out some of the demos.", "categories": [], "tags": [], "url": "/OpenGoPro/tutorials/camera-media-list#" diff --git a/contribution.html b/contribution.html index 9589ce52..266e70bb 100644 --- a/contribution.html +++ b/contribution.html @@ -520,10 +520,10 @@

Quiz

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

@@ -536,10 +536,10 @@

Quiz

- - @@ -555,10 +555,10 @@

Quiz

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

@@ -568,10 +568,10 @@

Quiz

- - @@ -594,7 +594,7 @@

Tabs

-
    +
    • tab1 @@ -609,7 +609,7 @@

      Tabs

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

        diff --git a/demos/bash/ota_update.html b/demos/bash/ota_update.html index 5f103a6a..c8f29e7b 100644 --- a/demos/bash/ota_update.html +++ b/demos/bash/ota_update.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -491,7 +491,7 @@

        Testing

        -

        Updated:

        +

        Updated:

        diff --git a/demos/c_c++/GoProC_C++Demo.html b/demos/c_c++/GoProC_C++Demo.html index e74836c0..38a81edc 100644 --- a/demos/c_c++/GoProC_C++Demo.html +++ b/demos/c_c++/GoProC_C++Demo.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -603,7 +603,7 @@

        Stream Commands

        -

        Updated:

        +

        Updated:

        diff --git a/demos/c_c++/GoProStreamDemo.html b/demos/c_c++/GoProStreamDemo.html index bfe03950..f6db0580 100644 --- a/demos/c_c++/GoProStreamDemo.html +++ b/demos/c_c++/GoProStreamDemo.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -406,7 +406,7 @@

        GoPro Low Latency St -

        Updated:

        +

        Updated:

        diff --git a/demos/csharp/GoProCSharpSample.html b/demos/csharp/GoProCSharpSample.html index 314b7947..9d3a203b 100644 --- a/demos/csharp/GoProCSharpSample.html +++ b/demos/csharp/GoProCSharpSample.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -435,7 +435,7 @@

        Usage

        -

        Updated:

        +

        Updated:

        diff --git a/demos/csharp/webcam.html b/demos/csharp/webcam.html index ff9abe7e..5573010d 100644 --- a/demos/csharp/webcam.html +++ b/demos/csharp/webcam.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -440,7 +440,7 @@

        Usage

        -

        Updated:

        +

        Updated:

        diff --git a/demos/python/multi_webcam.html b/demos/python/multi_webcam.html index fcf6f42f..6d4128a6 100644 --- a/demos/python/multi_webcam.html +++ b/demos/python/multi_webcam.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -554,7 +554,7 @@

        Module Usage

        -

        Updated:

        +

        Updated:

        diff --git a/demos/python/sdk_wireless_camera_control.html b/demos/python/sdk_wireless_camera_control.html index da6a0f1b..74f88480 100644 --- a/demos/python/sdk_wireless_camera_control.html +++ b/demos/python/sdk_wireless_camera_control.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -575,7 +575,7 @@

        GUI Demos

        -

        Updated:

        +

        Updated:

        diff --git a/demos/swift/EnableWiFiDemo.html b/demos/swift/EnableWiFiDemo.html index b452bbbf..6638e53c 100644 --- a/demos/swift/EnableWiFiDemo.html +++ b/demos/swift/EnableWiFiDemo.html @@ -34,7 +34,7 @@ - + @@ -344,7 +344,7 @@

        GoPro

        - +
        @@ -445,7 +445,7 @@

        Views

        -

        Updated:

        +

        Updated:

        diff --git a/feed.xml b/feed.xml index 1bb5551e..2980d9f9 100644 --- a/feed.xml +++ b/feed.xml @@ -1 +1 @@ -Jekyll2023-07-05T20:18:45+00:00https://gopro.github.io/OpenGoPro/feed.xmlOpen GoProOpen Source GoPro InterfaceGoPro \ No newline at end of file +Jekyll2023-08-15T20:37:01+00: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 5707e124..056fbd8e 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,63 +2,63 @@ https://gopro.github.io/OpenGoPro/demos/bash/ota_update -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/demos/c_c++/GoProC_C++Demo -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/demos/c_c++/GoProStreamDemo -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/demos/csharp/GoProCSharpSample -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/demos/csharp/webcam -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/demos/python/multi_webcam -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/demos/python/sdk_wireless_camera_control -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/demos/swift/EnableWiFiDemo -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/tutorials/connect-ble -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/tutorials/send-ble-commands -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/tutorials/parse-ble-responses -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/tutorials/ble-queries -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/tutorials/connect-wifi -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/tutorials/send-wifi-commands -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/tutorials/camera-media-list -2023-07-05T20:18:45+00:00 +2023-08-15T20:37:01+00:00 https://gopro.github.io/OpenGoPro/ble_2_0 diff --git a/tutorials/ble-queries.html b/tutorials/ble-queries.html index 3672e73a..27fba40b 100644 --- a/tutorials/ble-queries.html +++ b/tutorials/ble-queries.html @@ -34,7 +34,7 @@ - + @@ -410,7 +410,7 @@

        GoPro

        - +
        @@ -500,7 +500,7 @@

        Requirements

        Just Show me the Demo(s)!!

        -
          +
          • python @@ -511,7 +511,7 @@

            Just Show me the Demo(s)!!

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

              Setup

              characteristic. Throughout this tutorial, the query information that we will be reading is the Resolution Setting (ID 0x02).

              -
                + -
                  +
                  • Therefore, we have slightly changed the notification handler to update a global resolution variable as it @@ -757,8 +757,8 @@

                    Polling Query Information

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

                    - -Open GoPro user deviceGoProConnected (steps from connect tutorial)Get Setting value(s) command written to Query UUIDSetting values responded to Query Response UUIDMore setting values responded to Query Response UUID...More setting values responded to Query Response UUIDOpen GoPro user deviceGoPro +GoProOpen GoPro user deviceGoProOpen GoPro user device +Connected (steps from connect tutorial)Get Setting value(s) command 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 @@ -773,7 +773,7 @@

                    Individual Query Poll

                    First we send the query command:

                    -
                      +
                      • python @@ -784,7 +784,7 @@

                        Individual Query Poll

                      -
                        +
                        • The sample code can be found in in ble_query_poll_resolution_value.py. @@ -814,7 +814,7 @@

                          Individual Query Poll

                          When the response is received in / from the notification handler, we update the global resolution variable:

                          -
                            +
                            • python @@ -825,7 +825,7 @@

                              Individual Query Poll

                            -
                              +
                              • def notification_handler(handle: int, data: bytes) -> None:
                                @@ -878,7 +878,7 @@ 

                                Individual Query Poll

                                has changed:

                                -
                                  +
                                  • python @@ -889,7 +889,7 @@

                                    Individual Query Poll

                                  -
                                    +
                                    • INFO:root:Changing the resolution to Resolution.RES_2_7K...
                                      @@ -945,7 +945,7 @@ 

                                      Multiple Simultaneous Query Polls

                                      The query command now includes 3 settings: Resolution, FPS, and FOV.

                                      -
                                        +
                                        • python @@ -956,7 +956,7 @@

                                          Multiple Simultaneous Query Polls

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

                                            Multiple Simultaneous Query Polls

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

                                            -
                                              +
                                              • python @@ -992,7 +992,7 @@

                                                Multiple Simultaneous Query Polls

                                              -
                                                +
                                                • def notification_handler(handle: int, data: bytes) -> None:
                                                  @@ -1024,7 +1024,7 @@ 

                                                  Multiple Simultaneous Query Polls

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

                                                  -
                                                    +
                                                    • python @@ -1035,7 +1035,7 @@

                                                      Multiple Simultaneous Query Polls

                                                    -
                                                      +
                                                      • INFO:root:Received response at handle=62: b'0b:12:00:02:01:07:03:01:01:79:01:00'
                                                        @@ -1085,10 +1085,10 @@ 

                                                        Query All

                                                        Quiz time! 📚 ✏️

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

                                                        @@ -1101,10 +1101,10 @@

                                                        Query All

                                                        - -