MQTT#

For a brief introduction to MQTT, have a look at HiveMQ MQTT Essentials.

At startup the controller automatically connects by default to https://broker.hivemq.com, a fully open MQTT broker. If you want to use a different broker, see .env.example for the list of possible MQTT options.

Then it subscribes to commands messages published to the /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/0000/+/+/move_raw and /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/0000/+/+/rgb_led topics, among others. These topics are used to control the motors and on-board RGB LED.

They can be described as follows: /dotbots/<secret topic>/<swarm-id>/<dotbot-address>/<application>/<command> where:

  • secret topic is a base64 encoded topic, derived from a random 8 digits pin code using HKDF,

  • swarm-id is a 4 hexadecimal string (2B long) identifier corresponding to a swarm, typically all DotBots behind a single gateway

  • dotbot-address is a 18 hexadecimal string (8B long) unique identifier of a DotBot,

  • application is the type of application (0: DotBot, 1: SailBot)

  • command is the type of command (move_raw or rgb_led)

Since all messages are exchanged unauthentified via a public broker, all payloads exchanged between MQTT clients and the controller are encrypted using the standard JSON Web Encryption protocol. The symmetric keys used to encrypt the payload are also derived from a random 8 digits pin code using HKDF. All topics used by one controller and its PyDotBot clients use the same /dotbots/<secret topic> base topic to make sure multiple controller running at the same time won’t interfere.

One last thing about the 8 digit pin code: it rotates every 15 minutes (with a grace period of 2 minutes) to ensure it cannot reused later and to make brut force attacks harder. This means that every 15 minutes, the encryption key and base topic changes for a given controller. All clients are notified of this change and recomputes (or rederive) their key/topic accordingly.

Prerequisites#

Make sure you already followed the getting started page and have a functional setup with dotbot-controller running and connected to a nRF DK gateway.

To interact with the MQTT broker, you will use a Python script that require several packages:

  • paho-mqtt to connect and publish messages to the MQTT broker,

  • requests to directly fetch dotbots and the pin code from the controller REST api,

  • cryptography to derive the secret topic and encryption key using HKDF,

  • joserfc to encrypt the payload using JSON Web Encryption standard.

Install all the Python dependencies using pip:

pip install cryptography joserfc paho-mqtt requests

The basics#

Running the controller is as easy as running the following command:

dotbot-controller

The logs should contain information about the MQTT broker connection and the topic subscriptions:

Welcome to the DotBots controller (version: 0.17).
2024-01-11T13:42:02.738414Z [info     ] Lighthouse initialized         [pydotbot] context=dotbot.lighthouse2
2024-01-11T13:42:02.740025Z [info     ] Starting web server            [pydotbot] context=dotbot.controller
2024-01-11T13:42:02.752914Z [info     ] Serial port thread started     [pydotbot] context=dotbot.serial_interface
2024-01-11T13:42:02.949352Z [info     ] Connected                      [pydotbot] context=dotbot.mqtt flags=0 rc=0 receive_maximum=[10] topic_alias_maximum=[5]
2024-01-11T13:42:03.128297Z [info     ] Subscribed to /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/command/0000/+/+/move_raw [pydotbot] context=dotbot.mqtt qos=(0,)
2024-01-11T13:42:03.128606Z [info     ] Subscribed to /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/command/0000/+/+/rgb_led [pydotbot] context=dotbot.mqtt qos=(0,)
2024-01-11T13:42:03.128790Z [info     ] Subscribed to /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/command/0000/+/+/waypoints [pydotbot] context=dotbot.mqtt qos=(0,)
2024-01-11T13:42:03.128940Z [info     ] Subscribed to /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/command/0000/+/+/clear_position_history [pydotbot] context=dotbot.mqtt qos=(0,)
2024-01-11T13:42:03.129056Z [info     ] Subscribed to /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/lh2/add [pydotbot] context=dotbot.mqtt qos=(0,)
2024-01-11T13:42:03.129159Z [info     ] Subscribed to /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/lh2/start [pydotbot] context=dotbot.mqtt qos=(0,)
2024-01-11T13:42:03.129280Z [info     ] Subscribed to /dotbots/2SzQsZWfOV8OXrWQtEEdIA==/request [pydotbot] context=dotbot.mqtt qos=(0,)

In the output above you can see that the secret topic is 2SzQsZWfOV8OXrWQtEEdIA==.

Let’s start by fetching available dotbots and the pin code using our own Python script:

import requests

dotbots = requests.get('http://localhost:8000/controller/dotbots').json()

if not dotbots:
    print("No DotBot found!, exiting")
    sys.exit(0)

dotbot = dotbots[0]

if dotbot["status"] != 0:
    print("DotBot is not alive!, exiting")
    sys.exit(0)

dotbot_addr = dotbot["address"]
print(f"DotBot address: {dotbot_addr}")

pin_data = requests.get('http://localhost:8080/pin_code').json()
pin = str(pin_data["pin"]).encode()
print(f"Pin code: {pin.decode()}")

If you have a running DotBot, at this point you should have an output like this (with different address/pin values):

DotBot address: 9903ef26257feb31
Pin code: 30206157

Know let’s derive the secret topic and symmetric key using HKDF (extend the previous script with the following content):

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

from dotbot.protocol import PROTOCOL_VERSION

version = PROTOCOL_VERSION

# derive topic and key
kdf_topic = HKDF(
    algorithm=hashes.SHA256(),
    length=16,
    salt=b"",
    info=f"secret_topic_{version}".encode()
)
topic = base64.urlsafe_b64encode(kdf_topic.derive(pin)).decode()
print(f"Secret topic: {topic}")

kdf_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=b"",
    info=f"secret_key_{version}".encode()
)
key = kdf_key.derive(pin)
print(f"Encryption AES key: {key.hex()}")

To ensure consistent values on both ends the salt parameter is left empty and the info field contains a string built from the PyDotBot protocol version. This ensures different PyDotBot protocol versions cannot be used together.

At this point, when you run the script, you should have an output like:

DotBot address: 9903ef26257feb31
Pin code: 30206157
Secret topic: 2RIP5S_xgDvu6wGJVZH6tw==
Encryption AES key: ecddf00497b30b57d965310a46b0502e06ebe89374e4167f15fc06a44e9a06bf

We are now ready to add the MQTT client code to our script which is based on paho-mqtt:

import paho.mqtt.client as mqtt

# Connect to the MQTT broker
client = mqtt.Client(protocol=mqtt.MQTTv5)
client.tls_set_context(context=None)
client.connect("broker.hivemq.com", 8883, 60)

Change the color of the RGB LED#

Let’s change the RGB LED color of the DotBot by sending an rgb_led command. This command takes a payload parameter containing a json with the red, green and blue values to apply. But first the payload has to be encrypted using JWE. This can be done by extenting our script as follows:

import json
from joserfc import jwe

# Encryption using AESGCM
rgb_led = json.dumps({"red": 255, "green": 0, "blue": 0})
protected = {'alg': 'dir', 'enc': 'A256GCM'}
rgb_led_payload = jwe.encrypt_compact(protected, rgb_led, key)
print(f"RGB LED Payload: {rgb_led_payload}")

client.publish(f"/dotbots/{topic}/command/0000/{dotbot_addr}/0/rgb_led", rgb_led_payload)

And the RGB LED should turn red.

Move one DotBot#

Let’s now try to make the DotBot move forward briefly using the move_raw command:

move = json.dumps({"left_x": 0, "left_y": 80, "right_x": 0, "right_y": 80})
move_payload = jwe.encrypt_compact(protected, move, key)
print(f"Move Payload: {move_payload}")
client.publish(f"/dotbots/{topic}/command/0000/{dotbot_addr}/0/move_raw", move_payload, qos=1)

And the DotBot should move forward during 200ms!