14 minute read

This document will provide a walk-through tutorial to use the Open GoPro BLE Interface to send Type-Length-Value (TLV)commands and receive TLV responses.

Commands in this sense are operations that are initiated by either:

  • Writing to the Command Request UUID and receiving responses via the Command Response UUID.
  • Writing to the Setting UUID and receiving responses via the Setting Response UUID

A list of TLV commands can be found in the [Command ID Table]/OpenGoPro/ble/protocol/id_tables.html#command-ids).

This tutorial only considers sending these 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 connecting BLE tutorial are present and configured correctly.

It is suggested that you have first completed the connecting BLE tutorial before going through this tutorial.

Just Show me the Demo(s)!!

  • Each of the scripts for this tutorial can be found in the Tutorial 2 directory.

    Python >= 3.9 and < 3.12 must be used as specified in the requirements

    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
    

    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
    

    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
    

    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.

    kotlin_tutorial_2
    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 connecting BLE tutorial. In this case, however, we are defining a functional (albeit naive) notification handler that will:

  1. Log byte data and handle that the notification was received on
  2. Check if the response is what we expected
  3. 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.

  • async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> 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

All commands follow the same procedure:

  1. Write to the relevant request UUID
  2. Receive confirmation from GoPro (via notification from relevant response UUID) that request was received.
  3. 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 device devices 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 UUIDs to write to / receive responses from, which are:

  • We’ll define these and any others used throughout the tutorials and store them in a GoProUUID class:

    class GoProUuid:
        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")
        QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076")
        QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077")
        WIFI_AP_SSID_UUID = GOPRO_BASE_UUID.format("0002")
        WIFI_AP_PASSWORD_UUID = GOPRO_BASE_UUID.format("0003")
        NETWORK_MANAGEMENT_REQ_UUID = GOPRO_BASE_UUID.format("0091")
        NETWORK_MANAGEMENT_RSP_UUID = GOPRO_BASE_UUID.format("0092")
    
    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!

  • request_uuid = GoProUuid.COMMAND_REQ_UUID
    event.clear()
    request = bytes([3, 1, 1, 1])
    await client.write_gatt_char(request_uuid.value, request, response=True)
    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())
    
    We’re waiting to receive the data from the queue that is posted to in the notification handler when the response is received.

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:

  • Setting the shutter on
    Writing to GoProUuid.COMMAND_REQ_UUID: 03:01:01:01
    Received response at GoProUuid.COMMAND_RSP_UUID: 02:01:00
    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 UUID and the status was “success” (third byte == 0x00).

If you are recording a video, continue reading to 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.
  • await asyncio.sleep(2)
    request_uuid = GoProUuid.COMMAND_REQ_UUID
    request = bytes([3, 1, 1, 0])
    event.clear()
    await client.write_gatt_char(request_uuid.value, request, response=True)
    await event.wait()  # Wait to receive the notification response
    

    This will log in the console as follows:

    Setting the shutter off
    Writing to GoProUuid.COMMAND_REQ_UUID: 03:01:01:00
    Received response at GoProUuid.COMMAND_RSP_UUID: 02:01:00
    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())
    
    We’re waiting to receive the data from the queue that is posted to in the notification handler when the response is received.

    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

Now, let’s write the bytes to the “Command Request” UUID to change the preset group to Video!

  • request_uuid = GoProUuid.COMMAND_REQ_UUID
    request = bytes([0x04, 0x3E, 0x02, 0x03, 0xE8])
    event.clear()
    await client.write_gatt_char(request_uuid.value, request, response=True)
    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())
    
    We’re waiting to receive the data from the queue that is posted to in the notification handler when the response is received.

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:

Preset Group
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:

  • Loading the video preset group...
    Sending to GoProUuid.COMMAND_REQ_UUID: 04:3e:02:03:e8
    Received response at GoProUuid.COMMAND_RSP_UUID: 02:3e:00
    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 UUID and the status was “success” (third byte == 0x00).

Set the Video Resolution

The next command we will be sending is Set Setting to set the 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!

  • request_uuid = GoProUuid.COMMAND_REQ_UUID
    request = bytes([0x03, 0x02, 0x01, 0x09])
    event.clear()
    await client.write_gatt_char(request_uuid.value, request, response=True)
    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())
    
    We’re waiting to receive the data from the queue that is posted to in the notification handler when the response is received.

You should see the video resolution change to 1080 in the pill in the bottom-middle of the screen:

Video Resolution
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:

  • Setting the video resolution to 1080
    Writing to GoProUuid.SETTINGS_REQ_UUID: 03:02:01:09
    Received response at GoProUuid.SETTINGS_RSP_UUID: 02:02:00
    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 UUID and the status was “success” (third byte == 0x00). 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 Setting to set the 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. Other settings such as the current anti-flicker value may further limit possible FPS values. Futhermore, these capabilities all vary by camera. 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 Camera that is being operated on.

Now, let’s write the bytes to the “Setting Request” UUID to change the FPS to 240!

  • request_uuid = GoProUuid.COMMAND_REQ_UUID
    request = bytes([0x03, 0x03, 0x01, 0x00])
    event.clear()
    await client.write_gatt_char(request_uuid.value, request, response=True)
    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())
    
    We’re waiting to receive the data from the queue that is posted to in the notification handler when the response is received.

You should see the FPS change to 240 in the pill in the bottom-middle of the screen:

FPS
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:

  • Setting the fps to 240
    Writing to GoProUuid.SETTINGS_REQ_UUID: 03:03:01:00
    Received response at GoProUuid.SETTINGS_RSP_UUID: 02:03:00
    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 UUID and the status was “success” (third byte == 0x00). If the video resolution was higher, for example 5K, this would fail.

Quiz time! 📚 ✏️

Which of the following is not a real preset group?




True or False: Every combination of resolution and FPS value is valid.


True or False: Every camera supports the same combination of resolution and FPS values.


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 responses, proceed to the next tutorial.

Updated: