diff --git a/assets/js/lunr/lunr-store.js b/assets/js/lunr/lunr-store.js
index 97c20527..1c4b484d 100644
--- a/assets/js/lunr/lunr-store.js
+++ b/assets/js/lunr/lunr-store.js
@@ -519,21 +519,21 @@ var store = [
},
{
"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: 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.",
+ "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(characteristic: BleakGATTCharacteristic, data: bytes) -> None: logger.info(f'Received response at handle {characteristic.handle}: {data.hex(\":\")}') If this is the correct handle and the status is success, the command was a success if client.services.characteristics[characteristic.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): 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.",
+ "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(characteristic: BleakGATTCharacteristic, 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): 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.",
+ "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(characteristic: BleakGATTCharacteristic, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) 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(characteristic: BleakGATTCharacteristic, data: bytes) -> None: response.accumulate(data) Notify the writer if we have received the entire response if response.is_received: response.parse() If this is query response, it must contain a resolution value if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) 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(characteristic: BleakGATTCharacteristic, data: bytes) -> None: response.accumulate(data) if response.is_received: response.parse() if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: resolution = Resolution(response.data[RESOLUTION_ID][0]) fps = FPS(response.data[FPS_ID][0]) video_fov = VideoFOV(response.data[FOV_ID][0]) 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(characteristic: BleakGATTCharacteristic, data: bytes) -> None: logger.info(f'Received response at handle {characteristic.handle}: {data.hex(\":\")}') response.accumulate(data) Notify the writer if we have received the entire response if response.is_received: response.parse() If this is query response, it must contain a resolution value if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: global resolution resolution = Resolution(response.data[RESOLUTION_ID][0]) 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#"
diff --git a/contribution.html b/contribution.html
index 43b1a7b3..3b3cb3f3 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
-
-
- Correct!! 😃
- Incorrect!! 😭 The correct answer is A.
+
+
+ Correct!! 😃
+ Incorrect!! 😭 The correct answer is A.
And here is some more info
@@ -555,10 +555,10 @@
Quiz
%}
-
+
-
True or False?
-
+
True or False?
+
@@ -568,10 +568,10 @@
Quiz
-
-
- Correct!! 😃
- Incorrect!! 😭 The correct answer is True.
+
+
+ Correct!! 😃
+ Incorrect!! 😭 The correct answer is True.
And here is some more info
defnotification_handler(characteristic:BleakGATTCharacteristic,data:bytes)->None:response.accumulate(data)# Notify the writer if we have received the entire response
@@ -836,7 +836,7 @@
Individual Query Poll
response.parse()# If this is query response, it must contain a resolution value
-ifclient.services.characteristics[handle].uuid==QUERY_RSP_UUID:
+ifclient.services.characteristics[characteristic.handle].uuid==QUERY_RSP_UUID:resolution=Resolution(response.data[RESOLUTION_ID][0])
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
-
-
- Correct!! 😃
- Incorrect!! 😭 The correct answer is C.
+
+
+ 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’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
@@ -1191,7 +1191,7 @@
Registering for Query Push Not
First, let’s register for updates when the resolution setting changes:
Registering for Query Push Not
response.parse()# If this is query response, it must contain a resolution value
-ifclient.services.characteristics[handle].uuid==QUERY_RSP_UUID:
+ ifclient.services.characteristics[characteristic.handle].uuid==QUERY_RSP_UUID:globalresolutionresolution=Resolution(response.data[RESOLUTION_ID][0])
@@ -1305,7 +1305,7 @@
Registering for Query Push Not
the resolution changes. We verify this in the demo by then changing the resolution.
True or False: We can still poll a given query value while we are currently registered to
+
True or False: We can still poll a given query value while we are currently registered to
receive push notifications for it.
-
+
@@ -1382,20 +1382,20 @@
Registering for Query Push Not
-
-
- Correct!! 😃
- Incorrect!! 😭 The correct answer is A.
+
+
+ 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
+
True or False: A push notification for a registered setting will only ever contain query information
about one setting ID.
-
+
@@ -1405,10 +1405,10 @@
Registering for Query Push Not
-
-
- Correct!! 😃
- Incorrect!! 😭 The correct answer is B.
+
+
+ Correct!! 😃
+ Incorrect!! 😭 The correct answer is B.
It is possible for push notifications to contain multiple setting ID’s if both setting ID’s have
push notifications registered and both settings change at the same time.