Tutorial: Configure Single Device
This tutorial will show you how to:
- Propagate your custom configuration to a Device
- Report the current configuration and status of the Device to the Platform
Scenario
Imagine a robotic arm with two properties that you want to access from the Platform:
- Speed: You need to read it and adjust it remotely.
- Last maintenance date: Changing it requires a technician to visit the Device. You need to read it remotely to plan the next visit.
You'll use two JSON documents that the Platform stores for each Device:
- Desired Properties: You can update them, while the Device can only read them. The Platform propagates all the updates to the Device.
- Reported Properties: The Device can update them, while you can only read them.
The Device SDK also stores copies of these documents on the Device to handle Internet connection outages. The schema shows how you'll use the Desired and Reported Properties to configure the Device:
- You'll update the Desired Properties in the Platform.
- The Device SDK will read the updated Desired Properties and publish them to your program running on the Device.
- Your program will process the changes and update the Reported Properties using the Device SDK. The new version of the Reported Properties will contain the updated current speed and the last maintenance date.
- You'll verify that the Device has updated its configuration and state by checking the Reported Properties in the Platform.
Requirements
- PC or any other machine with either Linux or Windows.
- One of the following:
- Python of version ≥ 3.7
- C compiler (examples use GCC with Make on Linux and Visual Studio 2022 on Windows)
- Rust
- If you are not registered to the Spotflow IoT Platform yet, Sign Up.
- You need to have an existing Workspace and Provisioning Token.
1. Start Device
The program running on the Device will connect to the Platform and periodically synchronize its configuration and status.
- Python
- C
- Rust
If you haven't installed the Device SDK yet, run the following command:
pip install --upgrade spotflow-device
Paste the following code into a new file, or download it.
Replace the placeholder <Your Provisioning Token>
with your Provisioning Token and run the program.
If you don't have a Provisioning Token, see Create Provisioning Token for instructions on creating it.
import time
from spotflow_device import DesiredProperties, DeviceClient
DEFAULT_SPEED = 10
properties = {
"settings": {
"speed": DEFAULT_SPEED
},
"status": {
"lastMaintenance": "2024-02-04"
}
}
# Immediately print all changes of Desired Properties
def desired_properties_updated_callback(desired_properties: DesiredProperties):
print(f"[Callback] Received Desired Properties (version {desired_properties.version}): {desired_properties.values}")
# Connect to the Platform
client = DeviceClient.start(device_id="robo-arm", provisioning_token="<Your Provisioning Token>",
db="spotf_robo-arm.db", desired_properties_updated_callback=desired_properties_updated_callback)
# Synchronize the configuration and status
desired_properties_version = None
print("Setting Reported Properties to initial state")
client.update_reported_properties(properties)
while True:
desired_properties = client.get_desired_properties_if_newer(desired_properties_version)
if desired_properties is not None:
print(f"Received Desired Properties of version {desired_properties.version}")
if 'settings' in desired_properties.values and 'speed' in desired_properties.values['settings']:
print("Updating the speed to {}".format(desired_properties.values['settings']['speed']))
properties['settings']['speed'] = desired_properties.values['settings']['speed']
desired_properties_version = desired_properties.version
print("Updating Reported Properties")
client.update_reported_properties(properties)
print("Current speed: {}".format(properties['settings']['speed']))
time.sleep(5)
The program connects to the Platform using DeviceClient.start
.
Then, it uses the following methods of the DeviceClient
class:
get_desired_properties_if_newer
enables the program to poll for changes of the Desired Properties. Whenever you update the Desired Properties in the Platform, the Platform assigns a new version number to them. If the argument to the function isNone
, the function always returns the current version of the Desired Properties. If the argument is a version number, the function returns the current version only if it's newer than the specified one. As a result, the program processes the Desired Properties in the first iteration of the loop and then whenever they change.update_reported_properties
updates the Reported Properties of the Device in the Platform. If the Device is currently disconnected, the Device SDK will store the update and send it when the connection is restored.
If you don't want to use the polling, you can process the Desired Properties entirely using the callback provided to DeviceClientOptions.desired_properties_updated_callback
.
The Device SDK calls this callback right after DeviceClient.start
and then whenever the Desired Properties change.
Because the callback is called from a separate thread, you'll most likely need to use synchronization primitives when accessing data shared with the main thread.
See the code example: configure_device_async.py
Download the latest version of the Spotflow Device SDK library for your operating system and processor architecture:
Extract the archive to a directory of your choice.
Replace the contents of the file examples/get_started.c
with the following code (download):
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "jsmn.h"
#include "spotflow.h"
#ifdef _WIN32
#include <windows.h>
#define gmtime_r(timer, buf) gmtime_s(buf, timer)
#define usleep(x) Sleep((x)/1000)
#else
#include <unistd.h>
#endif
#define UNUSED(x) (void)(x)
#define PROPERTIES_BUFFER_SIZE 256
#define MAX_JSON_TOKENS 128
#define DEFAULT_SPEED 10
typedef struct {
double speed;
} properties_t;
bool json_find_object_property_value(const char* json_string, jsmntok_t* tokens, int num_tokens,
jsmntok_t* object_token, const char* key, jsmntok_t** value_token)
{
if (object_token->type != JSMN_OBJECT)
{
return false;
}
jsmntok_t* tokens_end = tokens + num_tokens;
jsmntok_t* token = object_token + 1;
while (token < tokens_end && token->start < object_token->end)
{
if (token->type == JSMN_STRING
&& token->end - token->start == (int)strlen(key)
&& strncmp(json_string + token->start, key, token->end - token->start) == 0)
{
*value_token = token + 1;
return true;
}
// Skip an uninteresting value even if it's an object or an array consisting of multiple tokens
token++;
jsmntok_t* skipped_value_token = token;
do
{
token++;
} while (token < tokens_end && token->start < skipped_value_token->end);
}
return false;
}
void properties_try_update_from_json(properties_t* properties, const char* json_string)
{
jsmn_parser parser;
jsmntok_t tokens[MAX_JSON_TOKENS];
jsmn_init(&parser);
int num_tokens = jsmn_parse(&parser, json_string, strlen(json_string), tokens, MAX_JSON_TOKENS);
if (num_tokens < 1)
{
return;
}
jsmntok_t* settings_token;
jsmntok_t* speed_token;
if (json_find_object_property_value(json_string, tokens, num_tokens, &tokens[0], "settings", &settings_token)
&& json_find_object_property_value(json_string, tokens, num_tokens, settings_token, "speed", &speed_token)
&& speed_token->type == JSMN_PRIMITIVE)
{
properties->speed = atof(json_string + speed_token->start);
}
}
void properties_serialize_json(properties_t* properties, char* buffer, size_t buffer_size)
{
snprintf(buffer, buffer_size, "{\"speed\":%.0f}", properties->speed);
}
void show_last_error()
{
size_t error_size = SPOTFLOW_ERROR_MAX_LENGTH;
char* error_buffer = malloc(error_size);
spotflow_read_last_error_message(error_buffer, error_size);
printf("Error: %s\n", error_buffer);
free(error_buffer);
}
// Immediately print all changes of Desired Properties
void desired_properties_updated_callback(const char* desired_properties, uint64_t version, void* _context)
{
UNUSED(_context);
printf("[Callback] Received Desired Properties (version %lu): %s\n", (unsigned long)version, desired_properties);
}
int main()
{
// The configuration is reset to DEFAULT_SPEED after every reboot
properties_t properties = { .speed = DEFAULT_SPEED };
spotflow_client_options_t* options;
spotflow_client_options_create(&options, "my-device", "<Your Provisioning Token>", "spotf_robo-arm.db");
spotflow_client_options_set_desired_properties_updated_callback(options, desired_properties_updated_callback, NULL);
spotflow_client_t* client;
if (spotflow_client_start(&client, options) != SPOTFLOW_OK)
{
show_last_error();
return 1;
}
uint64_t desired_properties_version = SPOTFLOW_PROPERTIES_VERSION_ANY;
char buffer[PROPERTIES_BUFFER_SIZE];
// Keep configuration in sync
while (1)
{
size_t desired_properties_length;
uint64_t new_desired_properties_version;
spotflow_client_get_desired_properties_if_newer(
client,
desired_properties_version,
buffer,
PROPERTIES_BUFFER_SIZE,
&desired_properties_length,
&new_desired_properties_version);
if (desired_properties_length > 0)
{
// Update the configuration
properties_try_update_from_json(&properties, buffer);
// Remember the version of the last configuration we received
desired_properties_version = new_desired_properties_version;
// Inform the platform about the new configuration
properties_serialize_json(&properties, buffer, PROPERTIES_BUFFER_SIZE);
spotflow_client_update_reported_properties(client, buffer);
}
printf("Current speed: %.0f\n", properties.speed);
usleep(1000 * 1000);
}
// This code is now unreachable, kept here for completeness
spotflow_client_destroy(client);
spotflow_client_options_destroy(options);
}
This example uses the library JSMN from Serge Zaitsev to parse JSON.
Download jsmn.h
to the examples
directory (where get_started.c
is located).
Replace the placeholder <Your Provisioning Token>
with your Provisioning Token.
If you don't have a Provisioning Token, see Create Provisioning Token for instructions on creating it.
Run the program:
- Linux
- Windows
Make sure that you have gcc
and make
installed.
Navigate to the directory examples/gcc_makefile_dynamic
and run the following command:
make run
Open the solution examples/vs2022_dynamic/spotflow_example.sln
in Visual Studio 2022.
Press F5 or Debug > Start Debugging to build and run the example program.
The program connects to the Platform using spotflow_client_start
.
Then, it uses the following functions:
spotflow_client_get_desired_properties_if_newer
enables the program to poll for changes of the Desired Properties. Whenever you update the Desired Properties in the Platform, the Platform assigns a new version number to them. If the argument to the function isNone
, the function always returns the current version of the Desired Properties. If the argument is a version number, the function returns the current version only if it's newer than the specified one. As a result, the program processes the Desired Properties in the first iteration of the loop and then whenever they change.spotflow_client_update_reported_properties
updates the Reported Properties of the Device in the Platform. If the Device is currently disconnected, the Device SDK will store the update and send it when the connection is restored.
If you don't want to use the polling, you can process the Desired Properties entirely using the callback provided to spotflow_client_options_set_desired_properties_updated_callback
.
The Device SDK calls this callback right after spotflow_client_start
and then whenever the Desired Properties change.
Because the callback is called from a separate thread, you'll most likely need to use synchronization primitives when accessing data shared with the main thread.
See the code example: configure_device_async.c
Run the following commands to create a new Rust project and add the dependencies including the crate spotflow
:
cargo new configure_device --bin
cd configure_device
cargo add anyhow serde_json spotflow
Replace the contents of src/main.rs
with the following code (download):
use anyhow::Result;
use serde_json::json;
use spotflow::{DeviceClientBuilder, PropertiesUpdatedCallback, VersionedProperties};
const DEFAULT_SPEED: u32 = 10;
struct DesiredPropertiesUpdatedCallback {}
impl PropertiesUpdatedCallback for DesiredPropertiesUpdatedCallback {
fn properties_updated(&self, properties: VersionedProperties) -> Result<()> {
// Immediately print all changes of Desired Properties
println!(
"[Callback] Received Desired Properties (version {}): {}",
properties.version, properties.values
);
Ok(())
}
}
fn main() {
let mut properties = json!(
{
"settings": {
"speed": DEFAULT_SPEED
},
"status": {
"last_maintenance": "2024-02-04"
}
}
);
// Connect to the Platform
let provisioning_token = String::from("<Your Provisiniong Token>");
let client = DeviceClientBuilder::new(
Some(String::from("robo-arm")),
provisioning_token,
"spotf_robo-arm.db",
)
.with_desired_properties_updated_callback(Box::new(DesiredPropertiesUpdatedCallback {}))
.build()
.expect("Unable to connect to the Platform");
// Synchronize the configuration and status
println!("Setting Reported Properties to initial state");
client
.update_reported_properties(&properties.to_string())
.expect("Unable to set Reported Properties");
let mut desired_properties_version = None;
loop {
let desired_properties = match desired_properties_version {
Some(version) => client.desired_properties_if_newer(version),
None => Some(
client
.desired_properties()
.expect("Error getting Desired Properties"),
),
};
if let Some(desired_properties) = desired_properties {
println!("Received Desired Properties of version {}", desired_properties.version);
let desired_properties_json =
serde_json::from_str::<serde_json::Value>(&desired_properties.values)
.expect("Unable to parse Desired Properties");
if desired_properties_json["settings"]["speed"].is_u64() {
let speed = desired_properties_json["settings"]["speed"]
.as_u64()
.unwrap();
println!("Updating the speed to {}", speed);
properties["settings"]["speed"] = speed.into();
}
desired_properties_version = Some(desired_properties.version);
println!("Updating Reported Properties");
client
.update_reported_properties(&properties.to_string())
.expect("Unable to update the Reported Properties");
}
println!("Current speed: {}", properties["settings"]["speed"]);
std::thread::sleep(std::time::Duration::from_secs(5));
}
}
Replace the placeholder <Your Provisioning Token>
with your Provisioning Token.
If you don't have a Provisioning Token, see Create Provisioning Token for instructions on creating it.
Run the program:
cargo run
The program connects to the Platform using DeviceClientBuilder::build
.
Then, it uses the following methods of the DeviceClient
struct:
desired_properties
returns the current Desired Properties of the Device.desired_properties_if_newer
enables the program to poll for changes of the Desired Properties. Whenever you update the Desired Properties in the Platform, the Platform assigns a new version number to them. The function returns the current version only if it's newer than the specified one.update_reported_properties
updates the Reported Properties of the Device in the Platform. If the Device is currently disconnected, the Device SDK will store the update and send it when the connection is restored.
If you don't want to use the polling, you can process the Desired Properties entirely using the struct provided to DeviceClientBuilder::with_desired_properties_updated_callback
.
The Device SDK calls this callback right after DeviceClientBuilder::build
and then whenever the Desired Properties change.
Because the callback is called from a separate thread, you'll most likely need to use synchronization primitives when accessing data shared with the main thread.
See the code example: configure_device_async.rs
When you run the program for the first time, it will show the details of the Provisioning Operation:
Provisioning operation initialized, waiting for approval.
Operation ID: fceb289b-33a2-42b0-92ed-785a377748f7
Verification Code: 9yreu8sy
After you approve the Device to the Platform (see Approve Device), the program will start writing the current speed every 5 seconds:
[Callback] Received Desired Properties (version 1): {}
Setting Reported Properties to initial state
Received Desired Properties of version 1
Updating Reported Properties
Current speed: 10
Current speed: 10
...
A newly registered Device has empty Desired Properties. Therefore, the program shouldn't depend on any of the Desired Properties being present.
Keep the program running and proceed to the next step.
2. Set Desired Properties
You'll now change the speed of the robotic arm using the Platform. Choose one of the following ways:
- Portal
- API
Login to the Spotflow IoT Platform Portal and follow the instructions:
Open the link Devices in the left sidebar.
Click the Device ID
robo-arm
to open its details. If the Device is not on the list, make sure that you have approved it earlier (see Approve Device).Scroll the page down to see Desired Properties and click Edit.
Update Content to
{"settings": {"speed": 20}}
and click Update.A box with the title Updated confirms that you've successfully updated the Desired Properties.
Replace these placeholders in the following command and run it:
<Your Workspace ID>
: The Portal page Workspaces shows the Workspace ID.<Your API Access Token>
: The Portal lets you obtain a short-term API access token. See the instructions.
- cURL
- PowerShell
curl -X PUT 'https://api.eu1.spotflow.io/workspaces/<Your Workspace ID>/devices/robo-arm/desired-properties' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <Your API Access Token>' \
-d '{"settings": {"speed": 20}}'
(Invoke-WebRequest -Method Put -Uri 'https://api.eu1.spotflow.io/workspaces/<Your Workspace ID>/devices/robo-arm/desired-properties' `
-Headers @{
'Content-Type' = 'application/json'
'Accept' = 'application/json'
'Authorization' = 'Bearer <Your API Access Token>'
} `
-Body '{"settings": {"speed": 20}}').Content
The API call updates the Desired Properties of the Device Twin and confirms the change by returning the new value (the JSON is formatted for better readability):
{
"settings": {
"speed": 20
}
}
After a few seconds, the program running on the Device will update its speed from the Desired Properties. The program will also update the Reported Properties to confirm the change:
...
Current speed: 10
[Callback] Received Desired Properties (version 2): {'settings': {'speed': 20}}
Received Desired Properties of version 2
Updating the speed to 20
Updating Reported Properties
Current speed: 20
Current speed: 20
...
You won't need to use the program anymore, so you can close it using Ctrl+C.
3. Inspect Reported Properties
You can now verify that the Device has updated its configuration from the Platform:
- Portal
- API
You should have the Device Details page open because of your previous interaction with the Portal. If not, navigate to the page by following the instructions in the previous step. Then:
Click the Refetch icon of the Reported Properties.
You should now see the updated Reported Properties.
The following instructions expect that you have already obtained the API access token from the Portal and that you know the ID of the Workspace you want to use.
Replace the placeholders <Your Workspace ID>
and <Your API Access Token>
with the respective values and run the following command:
- cURL
- PowerShell
curl -X GET 'https://api.eu1.spotflow.io/workspaces/<Your Workspace ID>/devices/robo-arm/reported-properties' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <Your API Access Token>'
(Invoke-WebRequest -Method Get -Uri 'https://api.eu1.spotflow.io/workspaces/<Your Workspace ID>/devices/robo-arm/reported-properties' `
-Headers @{
'Accept' = 'application/json'
'Authorization' = 'Bearer <Your API Access Token>'
}).Content
The result shows the last reported speed of the Device under the key speed
and additional information under the keys starting with $
.
When there are multiple properties, you can check when the Device updated each one of them:
{
"$metadata": {
"$lastUpdated": "2024-03-20T18:19:03.6541661Z",
"settings": {
"$lastUpdated": "2024-03-20T18:19:03.6541661Z",
"speed": {
"$lastUpdated": "2024-03-20T18:19:03.6541661Z"
}
},
"status": {
"$lastUpdated": "2024-03-20T18:12:28.5824456Z",
"lastMaintenance": {
"$lastUpdated": "2024-03-20T18:12:28.5824456Z"
}
}
},
"$version": 3,
"settings": {
"speed": 20
},
"status": {
"lastMaintenance": "2024-02-04"
}
}
Summary
Congratulations on finishing the tutorial! It has shown you how to configure a Device and obtain its status. Because the Platform and the Device SDK handle the synchronization of Desired and Reported Properties in the background, you don't have to deal with it in your application.
In the scenario, the section settings
in the Reported Properties has the same structure as in the Desired Properties.
However, you might want to use a different approach in your application.
For example:
- The Device can't update its speed based on your request because the value is out of allowed bounds. In this case, the Device can report this problem in the respective part of the Reported Properties.
- If you don't need to check that the Device has updated its configuration, you don't need to use the Reported Properties at all.
Desired and Reported Properties are designed for working with the current state of the Device. To store the history of the Device's operations, we instead recommend to send relevant logs, metrics, and traces to the Platform and route them to an OpenTelemetry observability backend.
What's Next
- The following tutorial shows how to use Tags and Device Fleet Configuration to configure multiple Devices at once.
- The parent page Configure Devices explains the concepts of Desired Properties and Reported Properties in more detail.
- The Device SDK references for Python, C, and Rust describe the interface you can use to handle the configuration from the Device side.
- The relevant parts of the API reference show how to gain more fine-grained control over the configuration. For example, you can update only a subset of the Desired Properties instead of replacing them.