With ClearBlade IoT Core, you can control a device by modifying its configuration. A device configuration is an arbitrary, user-defined data blob. After a configuration has been applied to a device, the device can report its state to ClearBlade IoT Core.

Device configuration works differently in the MQTT and HTTP bridges. See below for details.

For more information, see Devices, configuration, and state.

Limits

Configuration updates are limited to 1 update per second per device. However, for best results, device configuration should be updated much less often — at most, once every 10 seconds.

Protocol differences

MQTT

Devices using MQTT can subscribe to a special MQTT topic for configuration updates:

/devices/{device-id}/config

When a device subscribes to the configuration topic, the MQTT bridge responds with an MQTT SUBACK message, which contains the config topic’s granted QoS (0 or 1) or 128 if an error occurs.

After initially subscribing, the device receives the latest configuration in a message's payload. It will receive additional configuration updates as they are pushed to ClearBlade IoT Core.

Retrieving configuration updates on a device over MQTT code samples

Node.js
// const deviceId = `myDevice`;
// const registryId = `myRegistry`;
// const region = `us-central1`;
// const algorithm = `RS256`;
// const privateKeyFile = `./rsa_private.pem`;
// const serverCertFile = `./roots.pem`;
/**
 * @see https://clearblade.atlassian.net/wiki/spaces/IC/pages/2210299905/Retargeting+Devices#Production-URL-%2F-URLs for a full URL list
 */
// const mqttBridgeHostname = `us-central1-mqtt.clearblade.com`;
// const mqttBridgePort = 8883;
// const messageType = `events`;
// const numMessages = 5;

// The mqttClientId is a unique string that identifies this device. For ClearBlade
// IoT Core, it must be in the format below.
const mqttClientId = `projects/${projectId}/locations/${region}/registries/${registryId}/devices/${deviceId}`;

// With ClearBlade IoT Core, the username field is ignored, however it must be
// non-empty. The password field is used to transmit a JWT to authorize the
// device. The "mqtts" protocol causes the library to connect using SSL, which
// is required for Cloud IoT Core.
const connectionArgs = {
  host: mqttBridgeHostname,
  port: mqttBridgePort,
  clientId: mqttClientId,
  username: "unused",
  password: createJwt(projectId, privateKeyFile, algorithm),
  protocol: "mqtts",
  secureProtocol: "TLSv1_2_method",
  ca: [readFileSync(serverCertFile)],
};

// Create a client, and connect to the ClearBlade MQTT bridge.
const iatTime = parseInt(Date.now() / 1000);
const client = mqtt.connect(connectionArgs);

// Subscribe to the /devices/{device-id}/config topic to receive config updates.
// Config updates are recommended to use QoS 1 (at least once delivery)
client.subscribe(`/devices/${deviceId}/config`, { qos: 1 });

// Subscribe to the /devices/{device-id}/commands/# topic to receive all
// commands or to the /devices/{device-id}/commands/<subfolder> to just receive
// messages published to a specific commands folder; we recommend you use
// QoS 0 (at most once delivery)
client.subscribe(`/devices/${deviceId}/commands/#`, { qos: 0 });

// The MQTT topic that this device will publish data to. The MQTT topic name is
// required to be in the format below. The topic name must end in 'state' to
// publish state and 'events' to publish telemetry. This differs from the
// device registry's Cloud Pub/Sub topic.
const mqttTopic = `/devices/${deviceId}/${messageType}`;

client.on("connect", (success) => {
  console.log("connect");
  if (!success) {
    console.log("Client not connected...");
  } else if (!publishChainInProgress) {
    publishAsync(mqttTopic, client, iatTime, 1, numMessages, connectionArgs);
  }
});

client.on("close", () => {
  console.log("close");
  shouldBackoff = true;
});

client.on("error", (err) => {
  console.log("error", err);
});

client.on("message", (topic, message) => {
  let messageStr = "Message received: ";
  if (topic === `/devices/${deviceId}/config`) {
    messageStr = "Config message received: ";
  } else if (topic.startsWith(`/devices/${deviceId}/commands`)) {
    messageStr = "Command message received: ";
  }

  messageStr += Buffer.from(message, "base64").toString("ascii");
  console.log(messageStr);
});

client.on("packetsend", () => {
  // Logging packet send is very verbose
});

// Once all the messages have been published, the connection to ClearBlade
// IoT Core will be closed, and the process will exit. See the publishAsync method.

Python
import paho.mqtt.client as mqtt
import datetime
import ssl
import jwt

def create_jwt(project_id, private_key_file, algorithm):
    """Creates a JWT (https://jwt.io) to establish an MQTT connection.
    Args:
     project_id: The cloud project ID this device belongs to
     private_key_file: A path to a file containing an RSA256 or
             ES256 private key.
     algorithm: The encryption algorithm to use (RS256 or ES256)
    Returns:
        A JWT generated from the given project_id and private key, which
        expires in 20 minutes. After 20 minutes, your client will be
        disconnected, and a new JWT must be generated.
    Raises:
        ValueError: If the private_key_file does not contain a known key.
    """

    token = {
        # The time that the token was issued at
        "iat": datetime.datetime.now(tz=datetime.timezone.utc),
        # The time the token expires.
        "exp": datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(minutes=20),
        # The audience field should always be set to the GCP project id.
        "aud": project_id,
    }

    # Read the private key file.
    with open(private_key_file, "r") as f:
        private_key = f.read()

    print(
        "Creating JWT using {} from private key file {}".format(
            algorithm, private_key_file
        )
    )

    return jwt.encode(token, private_key, algorithm=algorithm)

def error_str(rc):
    """Convert a Paho error to a human readable string."""
    return "{}: {}".format(rc, mqtt.error_string(rc))


def on_connect(unused_client, unused_userdata, unused_flags, rc):
    """Callback for when a device connects."""
    print("on_connect", mqtt.connack_string(rc))

    # After a successful connect, reset backoff time and stop backing off.
    global should_backoff
    global minimum_backoff_time
    should_backoff = False
    minimum_backoff_time = 1


def on_disconnect(unused_client, unused_userdata, rc):
    """Paho callback for when a device disconnects."""
    print("on_disconnect", error_str(rc))

    # Since a disconnect occurred, the next loop iteration will wait with
    # exponential backoff.
    global should_backoff
    should_backoff = True


def on_publish(unused_client, unused_userdata, unused_mid):
    """Paho callback when a message is sent to the broker."""
    print("on_publish")


def on_message(unused_client, unused_userdata, message):
    """Callback when the device receives a message on a subscription."""
    payload = str(message.payload.decode("utf-8"))
    print(
        "Received message '{}' on topic '{}' with Qos {}".format(
            payload, message.topic, str(message.qos)
        )
    )

def get_client(
    project_id,
    cloud_region,
    registry_id,
    device_id,
    private_key_file,
    algorithm,
    ca_certs,
    mqtt_bridge_hostname,
    mqtt_bridge_port,
):
    """Create our MQTT client.

    The client_id is a unique string that identifies this device.
    For ClearBlade IoT Core, it must be in the format below.
    """
    client_id = "projects/{}/locations/{}/registries/{}/devices/{}".format(
        project_id, cloud_region, registry_id, device_id
    )
    print("Device client_id is '{}'".format(client_id))

    client = mqtt.Client(client_id=client_id)

    # With ClearBlade IoT Core, the username field is ignored, and the
    # password field is used to transmit a JWT to authorize the device.
    client.username_pw_set(
        username="unused", password=create_jwt(project_id, private_key_file, algorithm)
    )

    # Enable SSL/TLS support.
    client.tls_set(ca_certs=ca_certs, tls_version=ssl.PROTOCOL_TLSv1_2)

    # Register message callbacks. https://eclipse.org/paho/clients/python/docs/
    # describes additional callbacks that Paho supports. In this example, the
    # callbacks just print to standard out.
    client.on_connect = on_connect
    client.on_publish = on_publish
    client.on_disconnect = on_disconnect
    client.on_message = on_message

    # Connect to the MQTT bridge.
    client.connect(mqtt_bridge_hostname, mqtt_bridge_port)

    # This is the topic on which the device will receive configuration updates.
    mqtt_config_topic = "/devices/{}/config".format(device_id)

    # Subscribe to the config topic.
    client.subscribe(mqtt_config_topic, qos=1)

    # The topic on which the device will receive commands.
    mqtt_command_topic = "/devices/{}/commands/#".format(device_id)

    # Subscribe to the commands topic, QoS 1 enables message acknowledgment.
    print("Subscribing to {}".format(mqtt_command_topic))
    client.subscribe(mqtt_command_topic, qos=0)

    return client

get_client(
    "YOUR_PROJECT_ID",
    "us-central1",
    "YOUR_REGISTRY_ID",
    "YOUR_DEVICE_ID",
    "YOUR_DEVICE_PRIVATE_KEY_FILE",
    "RS256",
    "DigiCertGlobalRootCA.crt.pem",
    "us-central1-mqtt.clearblade.com",
    8883
)

HTTP

Devices must explicitly request new configurations if you're using the HTTP bridge.

Retrieving configuration updates on a device over HTTP code samples

Node.js
import fetch from 'node-fetch';

/**
 *
 * @see https://clearblade.atlassian.net/wiki/spaces/IC/pages/2202697766/Authenticating+applications for information on how to obtain your registry's urlBase, systemKey, and authToken
 *
 * @param {string} urlBase
 * @param {string} systemKey
 * @param {string} authToken
 * @param {string} deviceName
 * @param {number} version
 */
const getConfig = async (urlBase, systemKey, authToken, deviceName, version) => {
  const res = await fetch(
    `${urlBase}/api/v/4/webhook/execute/${systemKey}/cloudiotdevice_devices?name=${encodeURIComponent(
      deviceName
    )}&localVersion=${version}`,
    {
      method: 'GET',
      headers: {
        'ClearBlade-UserToken': authToken
      }
    }
  );
  const data = await res.json();
  console.log('Received config', data);
};

Python
# see https://clearblade.atlassian.net/wiki/spaces/IC/pages/2202697766/Authenticating+applications

import requests
import urllib.parse

def get_config(urlBase, systemKey, authToken, deviceName, version):
  url = f"{urlBase}/api/v/4/webhook/execute/{systemKey}/cloudiotdevice_devices"
  headers = { "ClearBlade-UserToken": authToken }
  params = { "name": urllib.parse.quote(deviceName), "localVersion": version }
  response = requests.get(url=url, headers=headers, params=params)
  if response.status_code == 200:
    return response.json()
  else:
    return response.text

# To obtain the 3 params below see https://clearblade.atlassian.net/wiki/spaces/IC/pages/2202697766/Authenticating+applications
# urlBase named 'URL'
# systemKey named 'System Key'
# authToken named 'Token'
urlBase = "URL_FOR_YOUR_REGISTRY"
systemKey = "SYSTEM_KEY_FOR_YOUR_REGISTRY"
authToken = "TOKEN_FOR_YOUR_REGISTRY"
deviceName = "your-device-name"
version = "LOCAL_CONFIG_VERSION_OF_YOUR_DEVICE"
config_resp = get_config(urlBase, systemKey, authToken, deviceName, version)
print(config_resp)

Updating and reverting device configuration

Console

Update the device configuration

1. Go to the ClearBlade IoT Core console’s Registries page.

2. Click the registry ID that contains the device.

3. In the left registry menu, click Devices.

4. Click the device ID whose configuration you want to update.

5. Click the ellipsis (⋮) at the top-right of the page, then select Update device.

6. Select the configuration’s format (text, Base64) and paste the data in the configuration box.

7. Click SEND TO DEVICE.

See Protocol differences for how devices receive the new configuration.

Revert the device configuration to a previous version

1. Go to the Google Cloud console’s Registries page.

2. Click the registry ID that contains the device whose configuration you want to revert.

3. In the left registry menu, click Devices.

4. Click the device ID whose configuration you wish to revert.

5. Click CONFIGURATION & STATE. Use the checkboxes to display configuration history, state history, or both. By default, both are shown.

6. Click a row to get the full configuration or state data in JSON and the timestamp and version.

7. Click COMPARE to compare the configuration data with the state data. This view can help you debug configurations and ensure devices have acknowledged specific configuration versions if you use MQTT (the HTTP bridge does not support configuration acknowledgments).

8. Click the version you want to revert to. Select Text in the Format list to see a text version of the configuration details.

9. Click Revert, ensure you've selected the correct version, then click Revert.

API

To update or revert device configuration via the API, use the device modifyCloudToDeviceConfig method, specifying the new or previous configuration in the config field. You can specify a configuration when creating a device and then use modifyCloudToDeviceConfig to change it later.

Updating a device's configuration code samples

Node.js
// const cloudRegion = 'us-central1';
// const deviceId = 'my-device';
// const projectId = 'adjective-noun-123';
// const registryId = 'my-registry';
// const data = 'test-data';
// const version = 0;

import { DeviceManagerClient } from '@clearblade/iot';

const iotClient = new DeviceManagerClient({
  // optional auth parameters.
});

async function modifyCloudToDeviceConfig() {
  // Construct request
  const formattedName = iotClient.devicePath(projectId, cloudRegion, registryId, deviceId);

  const binaryData = Buffer.from(data).toString('base64');
  const request = {
    name: formattedName,
    versionToUpdate: version,
    binaryData: binaryData
  };

  const [response] = await iotClient.modifyCloudToDeviceConfig(request);
  console.log('Success:', response);
}

modifyCloudToDeviceConfig();

C#
if (bPatchDeviceConfig)
{
    logger.LogInformation("Update configuration of a device");

    // While running this sample, it is assumed that device with name
    // "Sample-New-Device" exists

    string name = "projects/developmentenv/locations/us-central1/registries/Sample-New-Registry/devices/Sample-New-Device";

    var result = await mClient.GetDevice(4, name);
    if (!result.Item1 || (result.Item2 == null))
        logger.LogError("Failed to get a device configuration");
    else
    {
        logger.LogInformation("Successfully obtained the device configuration");

        // Use the obtained information
        string updateMask = "metadata";
        string pubKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5P0Z4OUD5PSjri8xexGo\n6eQ39NGyQbXamIgWAwvnAs/oDRVqEejE2nwDhnpykaCGLkuDEN0LPd2wF+vC2Cq3\nY3YvkJh71IkjuAjMZQ+00CXdezfCjmTtEpMCNA3cV+G1g6uIcdEpHKs0YHfC9CFQ\nrjkc7tl3idmcQLngIov/gsFY7D1pbOgkCVVcZCRLgsdFfhCUYwYCvdEVJP3w+5mG\nybvmhNRbbFG7eG3+hmZoOg0h3f6r2fqgSx6l0+Z3D77SRT6lBEHvGDlxb08ASeuE\n0SJAc6PdAKd3FDqdZok4z1qJsgMqtU/ZGJJG54pNECWmhoOar+aQmmqnZ6kGQ5cn\nEwIDAQAB\n-----END PUBLIC KEY-----\n";
        result.Item2.credentials.Add(new core.Models.Credential
        {
            expirationTime = "",
            publicKey = new PublicKey
            {
                format = "RSA_PEM",
                key = pubKey
            }
        });

        result = await mClient.PatchDevice(4, name, updateMask, result.Item2);

        if (!result.Item1 || (result.Item2 == null))
            logger.LogError("Failed to update a device configuration");
    }
}

Python
import os
import base64
from clearblade.cloud import iot_v1

def modify_cloud_to_device_config():
  project_id = 'YOUR_PROJECT_ID'
  cloud_region = 'us-central1'
  registry_id = 'your-registry-id'
  device_id = 'your-device-id'
  
  client = iot_v1.DeviceManagerClient()

  device_path = client.device_path(project_id, cloud_region, registry_id, device_id)

  # versionToUpdate MUST be the latest version. Setting this to 0 implies the latest version
  # since actual versions start with 1. If versionToUpdate is NOT 0 or the latest version, an
  # error is thrown.
  versionToUpdate = 0
  data = b"test-config-data"

  request = iot_v1.ModifyCloudToDeviceConfigRequest(
    name=device_path,
    version_to_update=versionToUpdate,
    binary_data=base64.b64encode(data)
  )

  client.modify_cloud_to_device_config(request=request)

os.environ["CLEARBLADE_CONFIGURATION"] = "/path/to/your-credentials.json"
modify_cloud_to_device_config()

Go
// setConfig sends a configuration change to a device.
func setConfig(w io.Writer, projectID string, region string, registryID string, deviceID string, configData string) (*iot.DeviceConfig, error) {
    ctx := context.Background()
    service, err := iot.NewService(ctx)

    if err != nil {
        return nil, err
    }

    req := iot.ModifyCloudToDeviceConfigRequest{
        BinaryData: b64.StdEncoding.EncodeToString([]byte(configData)),
    }

    path := fmt.Sprintf("projects/%s/locations/%s/registries/%s/devices/%s", projectID, region, registryID, deviceID)
    response, err := service.Projects.Locations.Registries.Devices.ModifyCloudToDeviceConfig(path, &req).Do()
    if err != nil {
        return nil, err
    }

    fmt.Fprintf(w, "Config set!\nVersion now: %d\n", response.Version)

    return response, nil
}

Reviewing device configuration

Console

1. Go to the Google Cloud console’s Registries page.

2. Click the registry ID that contains the device whose configuration you want to update.

3. In the left registry menu, click Devices.

4. Click the device ID whose configuration you want to update.

5. Click CONFIGURATION & STATE. Use the checkboxes to display configuration history, state history, or both. By default, both are shown.

6. Click a row to get the full configuration or state data in JSON and the timestamp and version.

7. Click COMPARE to compare the configuration data with the state data. This view can help you debug configurations and ensure devices have acknowledged specific configuration versions if you use MQTT (the HTTP bridge does not support configuration acknowledgments).

To review device configuration via the API, use a device configVersions.list request.

Review a device’s configuration code samples

Node.js
// const cloudRegion = 'us-central1';
// const deviceId = 'my-device';
// const projectId = 'adjective-noun-123';
// const registryId = 'my-registry';
import { DeviceManagerClient } from '@clearblade/iot';

const iotClient = new DeviceManagerClient({
  // optional auth parameters.
});

async function listDeviceConfigVersions() {
  // Construct request
  const devicePath = iotClient.devicePath(projectId, cloudRegion, registryId, deviceId);

  const [response] = await iotClient.listDeviceConfigVersions({
    name: devicePath
  });
  const configs = response.deviceConfigs;

  if (configs.length === 0) {
    console.log(`No configs for device: ${deviceId}`);
  } else {
    console.log('Configs:');
  }

  for (let i = 0; i < configs.length; i++) {
    const config = configs[i];
    console.log('Config:', config, '\nData:\n', config.binaryData.toString('utf8'));
  }
}

listDeviceConfigVersions();

C#
if (bGetDeviceConfig)
{
    logger.LogInformation("Get configuration of a device");

    // While running this sample, it is assumed that a device with name
    // "Sample-New-Device" exists and the version is updated to "2"

    string name = "projects/developmentenv/locations/us-central1/registries/Sample-New-Registry/Devices/Sample-New-Device";
    string localVersion = "2";

    var result = await mClient.GetDeviceConfig(4, name, localVersion);
    if (!result.Item1 || (result.Item2 == null))
        logger.LogError("Failed to get a device configuration");
    else
    {
        logger.LogInformation("Successfully obtained the device configuration");

        // Use the obtained information
    }
}

Python
import os
from clearblade.cloud import iot_v1

def list_device_config_versions():
  project_id = 'YOUR_PROJECT_ID'
  cloud_region = 'us-central1'
  registry_id = 'your-registry-id'
  device_id = 'your-device-id'
  
  client = iot_v1.DeviceManagerClient()

  device_path = client.device_path(project_id, cloud_region, registry_id, device_id)

  request = iot_v1.ListDeviceConfigVersionsRequest(
    name=device_path,
    numVersions=0
  )

  response = client.list_device_config_versions(request=request)
  configs = response.device_configs

  if (len(configs) == 0):
    print(f"No configs for device: {device_id}")
  else:
    print("Configs:")
  
  for config in configs:
    if config.binary_data is None:
      print(f"version: {config.version}\n\tdata: {None}")
    else:  
      print(f"version: {config.version}\n\tcloudUpdateTime: {config.cloud_update_time}\n\tdata: {config.binary_data}")

os.environ["CLEARBLADE_CONFIGURATION"] = "/path/to/your-credentials.json"
list_device_config_versions()

Go
// getDeviceConfigs retrieves and lists device configurations.
func getDeviceConfigs(w io.Writer, projectID string, region string, registryID string, device string) ([]*iot.DeviceConfig, error) {
    ctx := context.Background()
    service, err := iot.NewService(ctx)

    if err != nil {
        return nil, err
    }

    path := fmt.Sprintf("projects/%s/locations/%s/registries/%s/devices/%s", projectID, region, registryID, device)
    response, err := service.Projects.Locations.Registries.Devices.ConfigVersions.List(path).Do()
    if err != nil {
        return nil, err
    }

    for _, config := range response.DeviceConfigs {
        fmt.Fprintf(w, "%d : %s\n", config.Version, config.BinaryData)
    }

    return response.DeviceConfigs, nil
}