Better IoT design patterns: Desired state vs. actual state
Communicating with IoT devices over a network connection presents a few challenges you must consider when designing your firmware. The most obvious is how to deal with spotty connections. Did your device get the command you sent? Does the reported state in the cloud accurately reflect the actual device state?
At Golioth we’ve built a device cloud that eases the process of provisioning, connecting with, and controlling fleets of IoT devices. To get the most out of the platform consider using a few design patterns related to Digital Twin, a virtual representation of a device that is present in the cloud and provides a framework for interacting with the real hardware.
I’ll demonstrate this using the Golioth platform, but of course the concept is applicable to all connected devices.
Avoiding Confused Users and Confused Devices
While there are innumerable ways in the IoT realm to confuse your users and your devices (trust me, I’m trying to find them all), the most obvious is the challenge keeping the device and the cloud in sync with each other. For ease of understanding, let’s consider a box with one button and one LED that is connected to the internet over a cellular connection. We want the button to toggle the LED but also allow the cloud side to change the state of the LED. This might represent the state of some running service, but to keep it simple we’ll leave that out of the example.
If we rely on the cloud to control the device LED, the user will push the button and have to wait for the button press to make it to the cloud, wait for the state of LED on the cloud to be changed, and wait for this change to make its way back to the device to update the LED. The user is likely to think the button press was missed and press it again.
If the device directly controls the LED, what happens if the cloud misses the button press because of connectivity issues? When not carefully planned for, the state of the LED will be different from the state of the service in the cloud. What happens if the LED state on the cloud is changed at almost the same time the button is pressed? We have a race condition to see which update propagates the fastest.
One design pattern we should all be familiar with uses a separation of state from command/control so that the device and its digital twin will remain in sync even when there are latency or connection issues. A device just coming on line should be able to quickly enter a desired state from the server, and report back the actual state.
Keeping Desired State and Actual State Separate
The issues outline above can be avoided by using different endpoints for a device to communicate with its virtual equivalent.
- On the cloud there will be one representation where the physical device reports its current state. For the cloud this data is read-only, only the device can make updates to it.
- A separate representation is made for the desired state of the device. The cloud side can make changes to this and the physical device will watch for and react to these changes. The physical device usually has a mechanism that indicates the desired operations were received.
Our simple example only has one “actual state” variable that keeps track of the LED state. A server might watch this data via a WebSocket to toggle some service as the value changes.
The cloud can populate the “desired state” with endpoints that request changes to the LED status. This might happen when some service the cloud is watching changes state and the IoT device needs to be aware of that.
Demo Using Golioth
Golioth stores persistent data using LightDB state. We can use a simple data structure to represent the digital twin of our hypothetical IoT device:
{ "state": { "led": 0 }, "cmd": { "led_on": 1649691282 } }
The state
endpoint stores the last-reported state (on) of the device’s LED. The cmd
endpoint reflects the cloud’s desire for that LED to be on by setting the led_on
key to the current timestamp. The cloud could also set led_off
to a timestamp, and when the device acts upon the desired state, it can compare timestamps for all existing keys to establish which command is the most recent.
I like to have the IoT device delete the command keys once they are received (another approach would be to have the device set an acknowledged
value to the timestamp the desired state was serviced). In the above example, since the led_on
key exists, we assume the device has not yet seen it. Once it does, the device will turn on the LED, update the status key for that LED to 1, and delete the cmd/led_on
endpoint.
Our app can be set up to watch the desired state by observing the cmd
endpoint and running an on_update
function when a change is detected.
err = golioth_lightdb_observe(client, GOLIOTH_LIGHTDB_PATH("cmd"), COAP_CONTENT_FORMAT_TEXT_PLAIN, observe_reply, on_update);
The IoT device will write a simple string of “0” or “1” as the actual state when the button is pressed, and each time the device acts upon a desired state request.
char sbuf[2] = "0"; if (led_state & 0x01) { sbuf[0] = '1'; } int err = golioth_lightdb_set(client, GOLIOTH_LIGHTDB_PATH("state/led"), COAP_CONTENT_FORMAT_TEXT_PLAIN, sbuf, strlen(sbuf));
So let’s imagine that our IoT device is in power-saving mode. When it wakes up and connects to the network, it will immediately see the command endpoint is requesting an LED state change. Using this design pattern means your desired state changes are not missed; even when a network connection is unavailable the device will be able to update to the most recent commands the next time it connects.
You can try this out for yourself on the Golioth cloud right now. I’ve implemented the desired state pattern in sample code, and the Dev tier of Golioth lets you test 50 devices on our platform for free.
Further Learning
At its core, this design pattern is incredibly simple. When you put it into use, things may become more complex. Perhaps you want to have a command queue so that there is a history of changes for the device to review the next time it connects to a network. Instead of deleting the commands when acting upon them, a device may place an acknowledged timestamp next to them, or some other scheme that fits your needs. But at its core, the separation between a device reporting it’s state and the cloud requesting state changes is an excellent IoT approach, and a great way to start learning about the concept of digital twins.