Tracker Configuration

The Tracker provides a customizable configuration system:

  • Standard configuration allows features like publish intervals, sleep settings, etc. to be configured from the console.
  • Customized configuration makes it possible to extend this to your own custom parameters and custom tabs in the console!

Additionally:

  • Devices that are currently online receive the configuration updates immediately.
  • Devices that are offline, because of poor cellular coverage or use of sleep modes, receive configuration updates when they reconnect to the Particle cloud, if there were changes.
  • On device, the configuration is cached on the flash file system, so the last known configuration can be used before connecting to the cloud again.

Configuration

Configuration is scoped so you can have:

  • Fleet-wide configuration so all devices in your product have a common setting.
  • Per-device configuration that override settings for a specific device in your product (when marked as a development device).

In addition to setting values directly from the console, you can get and set values from the Particle Cloud API.

The configuration is hierarchical:

  • location
    • radius
    • interval_min
    • interval_max
    • min_publish
    • ...
  • imu_trig
    • ...
  • temp_trig
    • ...
  • rgb
    • ...
  • sleep
    • mode
    • exe_min
    • conn max
    • ...

Schemas

The configuration format is specified using JSON Schema. This section is not intended to be a complete reference to writing a schema, as there is literally a whole book devoted to this, available to read online for free at the link above. This should be enough to begin writing your own custom schema, however.

The schema serves several purposes:

  • Documenting the options
  • Instructing the console how to display the options
  • Validating the configuration

Using a custom schema, you can add additional configuration elements, either to an existing group (location, sleep, etc.) or to a new custom group for your product (recommended). The new tabs or options are presented to all team members who have access to the product.

Console

The settings panels in the fleet configuration and device configuration, including all of the built-in settings, are defined using the configuration schema.

You can also use this technique to create your own custom configuration panels!

This picture shows how elements in the schema directly map to what you can see in the console:

A custom schema replaces the existing schema. This means that as new features are added to Tracker Edge you will want to periodically merge your changes into the latest schema so you will get any new options.

Default Schema

This is the full schema for Tracker Edge, as of version 11. You won't need to understand the whole thing yet, but this is what it looks like:

 

Data types

The schema and support in Tracker Edge can include standard JSON data types, including:

  • Boolean values (true or false, a checkbox)
  • Integer values (optionally with a range of valid values)
  • Floating point values (optionally with a range of valid values)
  • Strings
  • Enumerations (a string with fixed options, a dropdown menu)
  • JSON objects (of the above types, with some limitations)

There is a limit to the size of the data, as it needs to fit in a 622-byte publish. You should keep the data of a reasonable size and avoid overly lengthy JSON key names for this reason.

Getting an access token

One easy way to get a temporary access token is to:

  • Open the console.
  • Open your Tracker product.
  • Click on Devices.
  • Open your device.
  • In the Events tab, click on View events from a terminal (it's a button).
  • Copy and paste the access token from the end of the command that is displayed.
  • This token is invalidated when your close the console.

You can also generate a token using oAuth client credentials. You can adjust the expiration time using this method, including making it non-expiring.

Backing up the schema

At this time, the schema can only be set using the Particle Cloud API. Examples are provided using curl, a common command-line program for making API calls.

It's a good idea to make a backup copy of the schema before you modify it. The feature to delete the custom schema and revert to the factory default is planned but not currently implemented.

curl -X GET 'https://api.particle.io/v1/products/:productId/config?access_token=:accessToken' -H 'Accept: application/schema+json' -o backup-schema.json
  • :productId with your product ID
  • :accessToken with a product access token, described above.

This will return a big block of JSON data and save it in the file backup-schema.json.

Or, the device-specific schema for a development device:

curl -X GET 'https://api.particle.io/v1/products/:productId/config/:deviceId?access_token=:accessToken' -H 'Accept: application/schema+json' -o backup-schema.json
  • :productId with your product ID
  • :deviceId with your Device ID that is set as a development device.
  • :accessToken with a product access token, described above.

Adding to the schema

"engine": {
    "$id": "#/properties/engine",
    "type": "object",
    "title": "Engine",
    "description": "CAN demo engine settings",
    "default": {},
    "properties": {
        "idle": {
            "$id": "#/properties/engine/properties/idle",
            "type": "integer",
            "title": "Idle RPM speed",
            "description": "If engine RPM is less than this value, the engine will be considered to be idling",
            "default": 1600,
            "examples": [
            ],
            "minimum":0,
            "maximum":10000
        },
        "fastpub": {
            "$id": "#/properties/engine/properties/fastpub",
            "type": "integer",
            "title": "Publish period when running (milliseconds)",
            "description": "Publish period when engine is not off or idling in milliseconds (0 = use default)",
            "default": 0,
            "examples": [
            ],
            "minimum":0,
            "maximum":3600000
        }
    }
}

Here's an example of adding a new Engine panel. Of note:

  • Since adding a custom schema replaces the default schema, you must include all of the elements from the default schema. It does not merge the two automatically for you. The whole file is included below.
  • The new engine block goes directly in line and below the tracker block, which is the last configuration block (at the time of writing).
  • The new engine configuration includes two elements: Idle RPM speed and Publish period when running (milliseconds); both are integers.

Here's the whole file so you can see exactly where the data goes when merged with the default schema.

 

Setting a custom schema

The full example code for the engine configuration above can be found in the AN017 Tracker CAN application note.

There is no UI for setting the configuration schema yet, you will need to set it using curl:

curl -X PUT 'https://api.particle.io/v1/products/:productId/config/:deviceId?access_token=:accessToken' -H  'Content-Type: application/schema+json' -d @engine-schema.json
  • :productId with your product ID
  • :deviceId with your Device ID that is set as a development device.
  • :accessToken with a product access token, described above.

To restore the normal behavior, instead of using @engine-schema.json, use @backup-schema.json you saved in the previous step.

Or, for product-wide configuration:

curl -X PUT 'https://api.particle.io/v1/products/:productId/config?access_token=:accessToken' -H  'Content-Type: application/schema+json' -d @engine-schema.json
  • :productId with your product ID
  • :accessToken with a product access token, described above.

Viewing in the console

This is what it looks like in the console:

Engine Settings

Setting configuration

You can also set the values using the API directly, such as by using curl:

curl -X PUT 'https://api.particle.io/v1/products/:productId/config/:deviceId?access_token=:accessToken' -H 'Content-Type: application/json' -d '{"engine":{"idle":1550,"fastpub":30000}}'

Be sure to update:

  • :productId with your product ID
  • :deviceId with your Device ID that is set as a development device. If you want to change the setting across your whole product leave off the slash and device ID.
  • :accessToken with a product access token, described above.

This sets this configuration object:

{
    "engine":{
        "idle":1550,
        "fastpub":30000
    }
}

Unlike the schema, you only need to set values that you are changing.

Getting configuration

curl -X GET 'https://api.particle.io/v1/products/:productId/config/:deviceId?access_token=:accessToken' -H "Accept: application/json" 

Be sure to update:

  • :productId with your product ID
  • :deviceId with your Device ID that is set as a development device. If you want to change the setting across your whole product leave off the slash and device ID.
  • :accessToken with a product access token, described above.

Firmware

And finally, this is how you access the data from your application firmware:

// Configuration settings, synchronized with the cloud
int fastPublishPeriod = 0;
int idleRPM = 1600;

Create some global variables for your settings.

// Set up configuration settings
static ConfigObject engineDesc("engine", {
    ConfigInt("idle", &idleRPM, 0, 10000),
    ConfigInt("fastpub", &fastPublishPeriod, 0, 3600000),
});
ConfigService::instance().registerModule(engineDesc);

In setup(), associate the variables with the location in the configuration schema. While just a couple lines of code, this automatically takes care of:

  • Loading the saved configuration from the file system during setup(), in case the device is offline.
  • When the device comes online, getting any updates that occurred while offline.
  • If the device is already online and the settings are changed, they are pushed to the device automatically.

For the full example, see the AN017 Tracker CAN, the CAN bus application note.

Example

Here's an example of how you set up a custom schema and use it from firmware. It includes many of the available types of data.

Schema - Example

    "mine": {
        "$id": "#/properties/mine",
        "type": "object",
        "title": "Mine",
        "description": "My custom settings",
        "default": {},
        "properties": {
          "contrast": {
            "$id": "#/properties/mine/properties/contrast",
            "type": "integer",
            "title": "Contrast",
            "description": "Display contrast 0 - 255",
            "default": 24,
            "examples": [
              10
            ],
            "minimum": 0,
            "maximum": 255
          },
          "tempLow": {
            "$id": "#/properties/mine/properties/tempLow",
            "type": "number",
            "title": "Low Temp",
            "description": "Low temperature setting, degrees C",
            "default": 0,
            "minimum": -100,
            "maximum": 200
          },
          "fruit": {
            "$id": "#/properties/rgb/properties/fruit",
            "type": "string",
            "title": "Fruit",
            "description": "Preferred fruit",
            "default": "apple",
            "enum": [
              "apple",
              "grape",
              "orange",
              "pear"
            ]
          },
          "message": {
            "$id": "#/properties/rgb/properties/message",
            "type": "string",
            "title": "Message",
            "description": "Custom display message",
            "default": ""
          },
          "thing": {
            "$id": "#/properties/mine/properties/thing",
            "type": "boolean",
            "title": "Enable Thing",
            "description": "Using a boolean to enable or disable a thing",
            "default": true
          }
        }
      }
    }

Here's the whole schema:

 

You can set it using curl or another tool to call the API:

curl -X PUT 'https://api.particle.io/v1/products/:productId/config?access_token=:accessToken' -H  'Content-Type: application/schema+json' -d @test-schema.json
  • :productId with your product ID
  • :accessToken with a product access token, described above.

Be sure to use the full schema, not just the part with "Mine" as a custom schema replaces the default schema!

Console - Example

This is what it looks like in the console.

Getting the Tracker Edge Firmware

The Tracker Edge firmware can be downloaded from Github:

https://github.com/particle-iot/tracker-edge

You will probably want to use the command line as there are additional commands you need to run after cloning the source:

git clone https://github.com/particle-iot/tracker-edge 
cd tracker-edge
git submodule update --init --recursive
  • Open Particle Workbench.
  • From the command palette, Particle: Import Project.
  • Run Particle: Configure Workspace for Device, select version 2.0.1, or later, Tracker, and your device.

main.cpp

This is the Tracker Edge main source file. There are only three lines (all containing "MyConfig") added to the default main.cpp.

 

MyConfig.h

The C++ header file for the custom configuration class.

 

MyConfig.cpp

The C++ implementation file for the custom configuration class.

 

Digging In - Example

Member variables in the C++ class

int32_t contrast = 12;
double tempLow = 0.0;
int32_t fruit = (int32_t) Fruits::APPLE;
String message;
bool thing = false;

The settings that you can configure in the console are all added as member variables in the MyConfig class.

Accessing member variables

int32_t contrast = MyConfig::instance().getContrast();

To access configuration settings, get the MyConfig instance, and call the accessor method getContrast(). There are accessors for all of the variables above.

int32_t getContrast() const { return contrast; };
double getTempLow() const { return tempLow; };
Fruits getFruit() const { return (Fruits)fruit; };
const char *getMessage() const { return message; };
bool getThing() const { return thing; };

C++ getter functions are provided for convenience in the .h file.

Defining an enumeration

enum class Fruits : int32_t {
    APPLE = 0,
    GRAPE,
    ORANGE,
    PEAR
};

The Fruits example is a bit more complicated. It's an enumeration. In the console, this shows up as a popup menu (combo box) with a pre-selected list of options. The data sent back and forth between the cloud and device and saved on the cloud side is a string.

However, in device firmware, it's sometimes easier to work with numeric constants instead of strings. The ConfigStringEnum takes care of mapping between numeric and string enumerations. It's optional - you can work directly with the strings if you prefer.

The declaration above creates a Fruits enumeration. MyConfig::Fruits::APPLE has a value of 0 as in int32_t. GRAPE is 1, and so on.

init() function

void MyConfig::init() {
    static ConfigObject mineDesc("mine", {
        ConfigInt("contrast", &contrast, 0, 255),
        ConfigFloat("tempLow", &tempLow, -100.0, 200.0),
        ConfigStringEnum(
            "fruit",
            {
                {"apple",  (int32_t) Fruits::APPLE},
                {"grape",  (int32_t) Fruits::GRAPE},
                {"orange", (int32_t) Fruits::ORANGE},
                {"pear",   (int32_t) Fruits::PEAR}
            },
            [this](int32_t &value, const void *context) {
                // Get fruit from class
                value = this->fruit;
                return 0;
            },
            [this](int32_t value, const void *context) {
                // Set fruit in class
                this->fruit = value;
                return 0;
            }
        ),
        ConfigString("message", 
            [this](const char * &value, const void *context) {
                // Get message from class
                value = message.c_str();
                return 0;
            },
            [this](const char * value, const void *context) {
                // Set message in class
                this->message = value;
                Log.info("set message to %s", value);
                return 0;
            }
        ),
        ConfigBool("thing", 
            [this](bool &value, const void *context) {
                // Get thing from class
                value = this->thing;
                return 0;
            },
            [this](bool value, const void *context) {
                // Set thing in class
                this->thing = value;
                Log.info("set thing to %s", value ? "true" : "false");
                return 0;
            }
        )
    });
    ConfigService::instance().registerModule(mineDesc);

    logSettings();
}

The init() method maps between the member variables and the configuration data. It also registers the module, which also:

  • Loads the configuration from the flash memory file system at startup, so the previously configured values are available even before the cloud has connected.
  • Upon connecting to the cloud, checks to see if there are configuration updates.
  • While connected, if the configuration changes, immediately updates the location configuration and saved data in the file system.

Simple mappings

ConfigInt("contrast", &contrast, 0, 255),
ConfigFloat("tempLow", &tempLow, -100.0, 200.0),

Some of these are very straightforward. These map the keys to the variables that hold the configuration data.

ConfigStringEnum

ConfigStringEnum(
    "fruit",
    {
        {"apple",  (int32_t) Fruits::APPLE},
        {"grape",  (int32_t) Fruits::GRAPE},
        {"orange", (int32_t) Fruits::ORANGE},
        {"pear",   (int32_t) Fruits::PEAR}
    },
    [this](int32_t &value, const void *context) {
        // Get fruit from class
        value = this->fruit;
        return 0;
    },
    [this](int32_t value, const void *context) {
        // Set fruit in class
        this->fruit = value;
        return 0;
    }
),

This is how the string enum is mapped to the actual enumeration contents. The last two items get and set the value in the class based on the string converted into an int32_t (integer) value. That is C++11 lambda syntax which allows you to define the function inline; the body of the function gets executed later.

[this](int32_t value, const void *context) {
    // Set fruit in class
    this->fruit = value;
    Log.info("fruit updated to %ld!", value);
    return 0;
}

One handy trick is that you can add more code to the setter so you will know when the value is updated by the cloud. In the contrast and lowTemp examples above the underlying value changes, but your code is not notified. Using a custom setter makes it easy to notify your code when a configuration change occurs.

ConfigString

ConfigString("message", 
    [this](const char * &value, const void *context) {
        // Get message from class
        value = message.c_str();
        return 0;
    },
    [this](const char * value, const void *context) {
        // Set message in class
        this->message = value;
        Log.info("set message to %s", value);
        return 0;
    }
),

You need to also provide a getter and setter for String variables so save the data in the underlying class. In this case, we use a String object so it's easy, but you can also use a pre-allocated buffer.

Singleton

The MyConfig class is modeled after the Tracker Edge classes that are a Singleton: there is only one instance of the class per application.

MyConfig::instance().init();

To use it, you get an instance of it using MyConfig::instance() and call the method you want to use, in this case init().

Like other Tracker Edge classes, you call:

Method Call it from
MyConfig::instance().init() setup()
MyConfig::instance().loop() loop()

In the C++ class, the variable to hold the instance is declared like this:

static MyConfig *_instance;

And there's an implementation of it at the top of the MyConfig.cpp file:

MyConfig *MyConfig::_instance;

Since it's a static class member, which is essentially a global variable, it's always initialized to 0 at startup.

MyConfig &MyConfig::instance() {
    if (!_instance) {
        _instance = new MyConfig();
    }
    return *_instance;
}

The function to get the instance checks to see if it has been allocated. If it has not been allocated, it will be allocated using new. This should happen during setup(). In either case, the instance is returned.