My original idea was essentially to build a programmable heating device that could be used for low-temperature cooking. Having a couple of RaspberryPi devices in a drawer I thought this could be an interesting and useful project. With Microsoft entering the world of Raspberry Pi with a RPi edition of Windows 10 IoT, the 'device' world became more accessible to me as a developer focused on Mcrosoft platform.
What I didn't know originally was how to solve the problem of controlling the device (as I didn't want to buy a touch-screen which would double the cost of the whole device) - and that was the point at which I ran into AllJoyn. In the times when the giants struggle to establish strong standards of communication of IoT devices, the opensource AllJoyn framework was probably one of the first more-or-less complete standards for IoT connectivity.
AllJoyn has had support from Microsoft and it was built into the first Windows IoT SDK, therefore it was a perfect platform for me to jump on with my project. While its future is currently vague, with emergence of new OpenConnectivity Foundation (co-formed by Microsoft), the ideas implemented in AllJoyn framework and its performance make it an interesting point of study for any engineer and maker. If AllJoyn becomes superseded by OpenConnectivity, it should be natural to port the project to this different connectivity platform.
Some essential concepts referenced by this project are listed below.
Sous-vide is a cooking technique based on heating poached products (mainly meats) to strictly controlled temperatures. In effect, uniformly processed, flavour-rich food is obtained (as all the juices and flavours are kept with the product in a vacuum-sealed bag), allowing for precise and repeatable cooking (eg. meat cannot be 'over-done').
Wikipedia provides a fairly comprehensive monography of sous-vide (https://en.wikipedia.org/wiki/Sous-vide).
As sous-vide is generally perceived as classy, gourmet-style cooking technique, mostly professional, upper-shelf (read: expensive) sous-vide heating appliances are available commercially. However, building such a device is not rocket science and seems to be a compelling challenge for a 'maker'.
AllJoyn is an opensource framework designed by Allseen Alliance (https://allseenalliance.org) in order to propose a unified platform for connectivity of IoT devices. It covers following aspects (among others):
- Device communication - by providing framework to define interfaces comprising Properties, Actions and Events
- Device advertisement and discovery - via a concept of a device service bus
- Device onboarding - ie. how do I plug a new device into my network
A fairly comprehensive documentation is provided on AllSeen Alliance portal (https://allseenalliance.org/framework/documentation) and is supplemented by samples included in AllJoyn SDKs.
How it was built
The challenge in sous-vide cooking is to heat poached food via immersing it in water for a prolonged time in a controlled temperature. The cooking device is actually a simple combination of:
- Temperature Sensor
- Device Control Unit implementing a PID controller - based on input from Temperature Sensor, the controller sets the power output by Heater component (a PID controller is the most typical and therefore default solution to this problem, a monography of a PID controller can also be found on Wikipedia - https://en.wikipedia.org/wiki/PID_controller)
- Water pump - in order to achieve uniform distribution of heat in the water, water needs to maintain constant circulation.
- User control unit - a UI device allowing to program the heating patterns
Note: vacuum-sealing of products for cooking can be achieved using cheap off-the-shelf appliances and is out-of-scope of this article.
AllJoyn framework - what does it give?
One of my main challenges in this project was to connect to the heating device from an external, mobile controller. I did assume I would use TCP-based connectivity, ie. my RPi would need to be connected to the network via WiFi, and I could have a mobile device (smartphone or tablet) connected to that network as well. So I did need to build some sort of an API exposed by the heating device, so that the controller UI could connect to it and send 'orders' as well as collect state (at least via polling). I had in mind some sort of a web-service (probably HTTP, REST-ful), which my controller would connect to. This would require configuring my controller with the service URL. MS samples do demonstrate publishing the device's API into Azure (obviously :) ), but that seemed inappropriate to me. Then I started reading about AllJoyn...
It turns out AllJoyn introduces the concept of a Device Service Bus, virtually assembled and exposed in the network by one or more AllJoyn Router nodes. Any AllJoyn "producer" device (an appliance) can dynamically locate the bus and publish it's interface to it. On the other hand, any AllJoyn "consumer" (a control panel device) can connect to the bus and query it for available devices.
Windows IoT comes with AllJoyn support, which means any Windows IoT device can serve as an AllJoyn Router. So my RPi cooking device can also publish the Alloyn Bus, and I can connect to it using a control panel written for Android.
In reality this means, as long as my heating device is in the same WiFi network as my control device is, they will instantly see each other and connect - and this project demonstrates such scenario. No user-provided API URL configuration is needed, etc.
Note: the RPi device still needs to be plugged into WiFi network, which means it requires preliminary setup (provision of WiFi SSID and Key). This makes the initial setup of the heating device more cumbersome - if I want to demo my device outside my own WiFi, I need either a screen, keyboard and mouse or at least an ethernet cable and remote PowerShell session to plug my device into the WiFi.
AllJoyn framework defines WiFi onboarding scenarios, where a UI-enabled 'onboarder' device is used to pass WiFi settings into a 'blind' UI-less 'onboardee' device, but I have not gone that far in this project.
Each of concept's components have been implemented using commonly available devices, as summarized below.
A simple portable immersion 500Watt heater ("travel heater") has been used. A similar appliance can be found eg. on eBay (http://www.ebay.co.uk/itm/500W-Electric-Water-Heater-Liquids-Immersion-Travel-Portable-Boiler-Element-Hot-/351790910762?hash=item51e85f4d2a:g:O64AAOSwARZXk0gW)
A popular DS18B20 digital temperature sensor in water-tight wrapping has been used. As this sensor communicates via OneWire protocol, plugging it directly as RPi's input is not feasible (no hardware support for OneWire in RPi). Therefore a OneWire-2-I2C bridge is required (as listed below).
Device control unit
Raspberry Pi 2 running Windows IoT and a software (SoViAgent) implementing a pseudo-PID controller algorithm and AllJoyn-based connectivity. The RPi is plugged into network using an official RPi WiFi dongle.
A simple aquarium pump has been used. As temperatures used do not exceed 70 C, a simple fish-tank device works reliably and seems to remain unaffected by heat.
- Two high-current relay switches to control the high-power devices (heater and pump) from Raspberry Pi's pin outputs.
- OneWire-2-I2C bridge device - based on DS2482 chip. Necessary to connect the Temperature Sensor to the WPi's I2C interface.
User control unit
A bespoke SoVi Controller application running on an Android smartphone or tablet, as described further in the article.
An assembled device (SoViAgent part) is visible below:
SoViAgent - Windows IoT app
The SoViAgent part of the solution is aggregated into SoViAgent.sln VisualStudio solution. It has the following components:
SoViAgent.UI - SoViAgent application project implemented as WinIoT Headed application (ie. with a UI). This application instantializes the SoViAgentService class which marshalls all communication between AllJoyn Bus and the device 'engine'.
SoViAgent.Headless - Placeholder for headless implementation of the SoViAgent.
SoViAgent.AllJoyn - The main application 'engine' with PID controller implementation and AllJoyn device wrapper classes.
SoViAgent.Devices - Implementation of hardware-specific input and output (eg. DS18B20 temp sensor communication wrapper).
org.stranger80.SoViAgent - AllJoyn connectivity impementation as generated by AllJoyn code generation tools.
SoViAgent.Test - Some rudimentary UnitTest classes.
Building an AllJoyn-enabled application for Windows IoT is supported by tooling provided in AllJoyn Studio package. The procedure is to generate proxy code based on an obtained or created interface definition - the Introspection XML. Code generator is used to generate C++ native code handling the AllJoyn communication details. This follows a typical 'contract-first' integration approach.
Note: a good step-by-step tutorial on building an AllJoyn-enabled application for Windows IoT can be found here: https://channel9.msdn.com/Blogs/Internet-of-Things-Blog/Using-the-AllJoyn--Studio-Extension
AllJoyn Interface 'contract' - Introspection XML
The Introspection XML is a way to define the 'API' exposed by the device on the AllJoyn device bus.
SoViAgent.xml in SoViAgent.AllJoyn project is defining the API for my cooking device. The XML must follow the D-Bus specification, and defines following fundamental elements of the interface:
- Methods - endpoints published by the device which can be used to send 'orders' into the device. For example:
Programs the device to start executing a heat pattern defined via array of 'heat points'.
<arg name="heatPointArray" type="a(di)" direction="in">
Array of heat points.
A 'heat point' is a single order to set a given temperature to T and maintain it for S seconds.
Thus it is a struct of 2 integers:
- Target temperature (degrees Celsius)
- Temperature sustain period (seconds)
<arg name="success" type="b" direction="out">
Result of operation (success = true/false).
Note how input and output arguments (
<arg> elements) are specified as sequences and patterns of values.
- Properties - attributes of a device which can be read and/or set by a client. For example:
<property name="Temperature" type="d" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
Current temperature measured by device (degrees Celsius).
Note that a property may signal changes in its value, and client application may listen for such signals rather then poll the device repeatedly.
- Events - actively signalled by the device, so that client applications may listen for event signals passively:
<signal name="DeviceErrorOccurred" sessioncast="true">
Signal to notify the controller about a device error.
<arg name="errorDescription" type="(is)" direction="out">
Error description struct:
- error code (integer)
- error description (string)
Once the AllJoyn plumbing code is generated from the Introspection XML, the
org.stranger80.SoViAgent contains the code handling the above elements. This code can then be referenced by C# wrappers (
SoViAgentService class) to expose the device functionality.
DS18B20 Temperature sensor - input
Rinsen.OneWire library (https://github.com/Rinsen/OneWire) to communicate with the temperature sensor. The code which wraps this is located in
Heater and Water pump - output
Both Heater and Water pump are on/off devices so can be controlled from RPi's output pins. The signal from the pins is passed into high-current relays which power the actual devices. This is handled in
PID Controller implementation
The 'brain' of temperature controller is the
DefaultController class, which works in 'ticks'. Each tick measures inputs from the input sensor, performs the output calculation according to hardcoded PID settings and sends updated output setting to the output device.
Headed vs. Headless
While this prototype has been implemented as a headed app only, it is worth noting a headless implementation would be more lightweight, and would possible increase device startup time. In any case, it is useful to configure the app to run automtically on RPi boot.
- For a headed app, one can make it a default app via Windows IoT's App Manager console. The application will start at boot if set to headed default mode.
- For a headless app it is possible to either set it as default in headless boot mode, or add to startup list in headed mode - this will make it run as a 'service':
SoViController - Android app
The code was written based on the 'Observer' example included in AllJoyn SDK for Android.
The Android application consists of following components:
SoViAgent interface and
org.stranger80.sovicontroller.model namespace - definition of the classes handling the device contract. These were written by hand - I didn't spend much time on trying to find a suitable gode generation tool, fortunately there isn't much to write here apart from a number of AllJoyn-specific annotations.
AllJoynBusHandler - responsible for all communication with the AllJoyn bus.
BusHandlerService - an Android Service class responsible for embedding the SoVi device communication in a background thread, detached from the UI activities.
DevicePanelActivity - displays the device control panel.
MainActivity - main app activity, responsible for searching for available devices and redirecting the user to actual SoViAgent instance's control panel.
Once the SoVi Controller connects to a SoVi Agent device, the control panel gets displayed:
A user may turn the 'knob' control to set the target temperature and then tap the 'knob' to switch the device on or off. As simple as that... :)
The panel gets updated with all changes of device properties, and the temperature gets plotted on a graph for illustration of the heating curve. It is possible to observe the effect of the PID Controller implementation - whe programmed to maintain a fixed temperature T the kontroller manages to keep the temperature within +-0.5 C range, which probably is as good as it get's with a simple on/off heater.
Note: at the moment the controller app does not handle the device notifications.
SoVi Agent in action
Finally it is possible to put the whole device into action! Below is a small pictorial of a sous-vide beef fillet cooking.
A small chunk of beef fillet is cut and seasoned:
Vacuum-sealed poaches are cooked using the SoVi Agent device in 52 degrees Celsius for 1 hour:
Each piece is then seared on a grilling pan for crispy skin and flavour:
...and cut into tasty-looking slices for serving:
All in all, I really enjoyed building this prototype. Several thoughts can be highlighted as lessons learned from this initiative:
- AllJoyn seems a really powerful framework, and the solution it provides does seem coherent and complete. While I didn't explore the areas of security and device onboarding, the framework covers them and exploring them is possible.
- AllJoyn implementations I dealt with (Windows and Android) seem really performant, which could be seen when observing delay between user actions in the controller and device reacting to them - a tap on the control knob triggers the heater on/off immediately. Impressive.
- AllJoyn seems fairly complicated to start with, and its support is limited.
- Granted I am not a specialist Android developer, writing the controller app with a requirement to maintain a constantly connected background service to handle the AllJoyn Bus traffic was a big struggle for me. I ended up with a spider's web of handlers and listeners, which seem to work (more-or-less) but I keep asking myself a question - is it possible to write it in a simpler way, or is Android's programming model so convoluted by design?
- The Universal Windows Platform is a framework which makes me raise eye brows. For example I wasn't able to write Unit Tests as a UWP assembly using my favourite mocking frameworks - FakeItEasy didn't have a compliant version to run on the UWP API (I believe the main reason is the lack of Reflection and code emitting APIs) - I ended up writing mocking classes myself from scratch, which was slightly disappointing.
- The code published is really 'prototype', ie. unstable (esp. the Android part).
- Future of AllJoyn is vague once Microsoft stepped on the OpenConnectivity Foundation boat. At least we can expect support for OpenConnectivity support in future Windows IoT versions.