IoT Dash - Device Discovery, Commanding and Data Channeling - CodeProject

:

Introduction

IoT-Dash will enable you to quickly implement device-discovery for prototyping purposes on any IoT platform that supports either the .NET Micro Framework or Node.js with a few lines of code and a very simple protocol that can be easily extended for your needs. The core app is a WPF-App running on Windows.

Background

Reading about the existing IoT and in general device-discovery standards I got curious about how to implement my own. Why? First because of the learning experience and second because something simple was needed to quickly prototype and test device-discovery on local  networks. The goal was to make the devices propagate themselfes on the network with a minimum of code required and a minimum of configuration as well. Another reason was, that I wanted to command my devices what to do and to be able to visualize their sensor (or whatever data is worth reading) data in a unified and extendable way. Of course the data should be available for further analysis. This is where the channels come into play.

 

IoTDash - the Main Application

I want to give you a quick overview of IoTDash  itself. Here is a screen-shot of what it looks like and what it offers:

IoTDash Overview

The main app is devided into the following areas (from top to bottom):

  • First, you have the "File" and "Tools" menu
  • Second, the discovery and channel area
  • Third, the device area

 

The File-Menu

The "File-Menu" gives you the possiblity to save and load sensor-data configurations. Once you have saved a configuration file you can load that configuration for the discovered devices. If a device within the configuration is not available - for whatever reason - the part of the configuration concerning this specific device, will not be applied.

The Tools-Menu

This menu let's you define the configuration file that should be loaded on application start and if anything should be pre-loaded at all.

It is also the place where you can configure your settings for the communication channels, that let you (currently) save (or send) your sensor data to:

  • SQL Server (local or Azure, simply depending on your connection string)
  • Push the event data to a SignalR-Hub
  • Save the data to an Azure Table (Azure Storage)
  • Or send the data to an Azure Event Hub for further mass-processing of data

The Discovery and Channel Area

In all cases, if you don't provide any configuration data (for example for sql server) the Comm-Channel will not be opened and activated, the "Discover" button will be blended-out during auto-discovery and the progress-bar on the right will indicate that the discovery is running. You can see also messages within the status bar during auto-discovery. Once the auto-discovery is finished (on startup) the "Discover" button will be visible again. Pressing this button (a Toggle-Button) will run the discovery process for additional devices you might have added.

If there is a valid configuration for any of the Comm-Channels, the "Start Comm Channels" button will be enabled. If not, it will be disabled. Once pressed, it will send the event data of all discovered devices to the pre-configured Comm-Channels. Pressing it again will stop the channeling threads.

The Device Area

This is where all of your discovered devices will be listed. On the left side you can see the list of the discovered device-types. Each device will be presented with the following base-informations:

  • The device-name
  • An image of the registered device
  • Wether it is a secure (SSL) or unsecure connection
  • If the device is still alive
  • And the IP-Adress of the device

As soon as you select one device from the list (after an auto-discovery, this will be the first device in the list), you will see the available device-commands and device-sensors on the left side. For each of the sensors graphical representations for the sensor-events (like GETTEMP) can be added. This visual representation currently is a simple user-control that displays the value of the sensor-event with a caption and sub-caption.

IoTDash in Action!

Here is a short YouTube video that will demonstrate the capabilíties of IoTDash and how to use it.

 

 

Implemenation Details

The Pseudo-Protocol

Here is the definition from WikiPedia what exactly a communication protocol is:

Quote:

In telecommunications, a communication protocol is a system of digital rules for data exchange within or between computers.

Communicating systems use well-defined formats (protocol) for exchanging messages. Each message has an exact meaning intended to elicit a response from a range of possible responses pre-determined for that particular situation. Thus, a protocol must define the syntax, semantics, and synchronization of communication; the specified behavior is typically independent of how it is to be implemented. A protocol can therefore be implemented as hardware, software, or both. Communication protocols have to be agreed upon by the parties involved.[1] To reach agreement, a protocol may be developed into a technical standard. A programming language describes the same for computations, so there is a close analogy between protocols and programming languages: protocols are to communications as programming languages are to computations.[2]

The "protocol" defined to be used  for IoTDash does quite NOT meet all of this required rules. But it has a basic and logical structure that requires some steps to be taken for a successfull discovery process. This includes specific sets of data and the implementation of specific commands every backend should support in order to be discoverable.

Let's go now through the required parts one by one.

System Features

In essence, sytem-features tell IoTDash, what kind of device has actually been discovered:

  • "Name" - the name of the device, for example "Fez Spider"
  • "Backend" - for example "netmf" or "nodejs"
  • "GetCommands" - The command to list available commands
  • "GetSensors" - the command to list the available sensors
  • "GetSysInfo" - the command to get this system informations

That way you can define your own command-phrases and still be certain, that the system will work. Except for one thing: GetSysInfo (for the moment) should be set always to "SysInfo".

Available Commands

If you have for example a relay attached to your board (the device running netmf or node.js), and you have connected for example a vent to that relay, you most probably want to turn that vent on or off, using a command. Commands are just string constants like:

  • "turnonvent"
  • "togglerelay"
  • "blink"
  • "stopblink"

and so on. It's up to you. Your device will listen to this commands and execute them as you order them to be executed.

Available Sensors

To add a layer of abstraction for any kind of data (coming from a real sensor or not), the sensor definition offers the freedom to define sensor names and types as you wish:

  • SensorId - for example S00
  • SensorType - for example DASS (Device Alive Sensor)

or

  • SensorId - S01
  • SensorType- TEMP

Available Sensor Events

Now, we come to the interesting part. For each sensor you can define the coupled events:

  • EventId - for example E00
  • EventName - for example ALIVE
  • EventDataType - for example INT
  • SensorName - for example S00

You can define as many events as you wish. This events and the coupled sensors, will later be made visible within IoTDash. That way you can query this events and add sensor-controls for them within IOTDash.

Each of the above listed procotol-parts are delivered as JSON for a simple and easy handling. Let's see now, how each of this parts is actually implemented in code.

The Pseudo-Protocol Parts Implementation using the .NET Micro-Framework

The configuration of the pseudo-protocol parts on the NetMF side is accomplished by implementing the interface IDiscoveryConfiguration (that way no specialized config-files are needed):

using System;
using Microsoft.SPOT;

namespace EasyDiscovery.Model
{
    public interface IDiscoveryConfiguration
    {

         string Commands { get; set; }

         string HostProperties { get; set; }

         string MESSAGE_DELIMITER {get; set;}

         string[] AvailableCommands { get; set; }

         string[] AvailableSystemCommands { get; set; }

         AvailableSensors AvailableSensors { get; set; }

         SensorEvents AvailableEvents { get; set; }

         SystemFeatures LocalSystemFeatures { get; set; }

      
        string GetSystemFeaturesJson();
        string GetAvailableCommandsJson();
        string GetAvailableSensorsJson();
        string GetAvailableEventsJson();
    }
}

To actually serialize and de-serialize JSON-Messages JSON.NetMF is used (please check the resources section for a link to JSON.NetMF). It is able to serialize objects and arrays of objects very efficiently and fast.

All you have to do, to configure your NetMF based device is to implement the above interface. Of course it depends on what kind of informations you want to propagate accross the network. Here is a sample implementation:

public  class DiscoveryConfiguration : IDiscoveryConfiguration
   {

       public   string Commands { get;set;}

       public   string HostProperties { get;set;}

       public  string MESSAGE_DELIMITER { get; set; }

       public   string[] AvailableCommands { get;set;}

       public  string[] AvailableSystemCommands { get; set; }

       public  AvailableSensors AvailableSensors { get; set; }



       public  SensorEvents AvailableEvents { get; set; }

       public  SystemFeatures LocalSystemFeatures { get; set; }

       //TODO: Add command chaining, not here, in the client! Based on events
       public  DiscoveryConfiguration()
       {

           MESSAGE_DELIMITER = "<EOF>";

           //SYSTEM FEATURES
           LocalSystemFeatures = new SystemFeatures();

           LocalSystemFeatures.Features.Add("Name","Fez Spider");
           LocalSystemFeatures.Features.Add("Backend", "netmf");
           LocalSystemFeatures.Features.Add("GetCommands", "ListCommands");
           LocalSystemFeatures.Features.Add("GetEvents", "ListEvents");
           LocalSystemFeatures.Features.Add("GetSensors", "ListSensors");
           LocalSystemFeatures.Features.Add("GetSysInfo", "SysInfo");

           //AVAILABLE COMMANDS
           AvailableCommands = new string[]{
                                               "blink",
                                               "blinkfast",
                                               "stopblink",
                                               "blinkgreen",
                                               "stopblinkfast",

                                           };

           //AVAILABLE SYSTEM COMMANDS
           AvailableSystemCommands = new string[]{

                                               "ListCommands",
                                               "ListEvents",
                                               "ListSensors",
                                               "SysInfo"
                                           };

           //SENSORS
           AvailableSensors = new AvailableSensors()
           {

               Sensors = new ArrayList
               {
                   //FIRST "SENSOR" THAT IS USED TO CHECK, IF THE DEVICE IS ALIVE!
                   {new Sensor() {SensorId = "S00", SensorType = "DASS"}},
                   {new Sensor() {SensorId = "S01", SensorType = "TEMP"}},
                   {new Sensor() {SensorId = "S02", SensorType = "MOIST"}}
               }
           };

            //SENSOR EVENTS
           AvailableEvents = new SensorEvents
           {
                Events = new ArrayList()
               {
                   //FIRST ENTRY IS A SYSTEM SENSOR. THIS EVENT SHOWS US, IF THE DEVICE IS STILL AVAILABLE!
                   { new SensorEvent() {EventId="E00" ,EventName = "ALIVE", EventDataType = "INT", SensorName = "S00"}},
                   { new SensorEvent() {EventId="E01" ,EventName = "GETTEMP", EventDataType = "DEC", SensorName = "S01"}},
                   { new SensorEvent() {EventId="E02", EventName = "GETMOIST", EventDataType = "DEC", SensorName = "S02"}}
               }
           };




       }

       public  string GetSystemFeaturesJson()
       {
           return JsonSerializer.SerializeObject(LocalSystemFeatures.Features);
       }

       public  string GetAvailableCommandsJson()
       {
           var commands = new AvailableCommands() {Commands = AvailableCommands};

           return JsonSerializer.SerializeObject(commands);
       }

       public  string GetAvailableSensorsJson()
       {
           return JsonSerializer.SerializeObject(AvailableSensors);
       }

       public  string GetAvailableEventsJson()
       {
           return JsonSerializer.SerializeObject(AvailableEvents);
       }
   }

You will find this implementation also within the source-code attached to this article. As you can see, the implementation is fairly simple and straight forward.

Let's now take a look, how the pseudo-protocol parts are defined in Node.js.

/***** COMMAND CONFIGURATION ****/
var commands ={Commands:[
        "blinkfast",
        "stopblink",
        "stopblinkfast",
	"onlyongalileo"]}
    ;

/****** HOST-PROPERTIES (SYSINFO)******/
var hostProps = 
     {
        "Name": "Raspberry PI B+",
        "Backend": "Node.js",
	"GetCommands": "ListCommands",
	"GetEvents": "ListEvents",
	"GetSensors": "ListSensors",
	"GetSysInfo": "SysInfo"

    };


/****** EVENTS ************/
var events = {  "Events":
	
	
		[{	"EventId":"E00",
			"EventName":"ALIVE",
			"EventDataType":"INT",
			"SensorName":"S00"
		},
		{
                        "EventId":"E0115",
                        "EventName":"GETTEMP",
                        "EventDataType":"DEC",
                        "SensorName":"S01"
                },
		{
                        "EventId":"E0215",
                        "EventName":"GETMOIST",
                        "EventDataType":"DEC",
                        "SensorName":"S02"
                }]
	};


/********** SENSORS *********/
var sensors = 
		{"Sensors":[
			{
			    "SensorId":"S00",
			    "SensorType":"DASS"
			    		
			},

			 {
                            "SensorId":"S01",
                            "SensorType":"TEMP"

                        },

			 {
                            "SensorId":"S02",
                            "SensorType":"MOIST"

                        }
		]};

Nothing too spectacular here to see as well. That's how you configure your Node.js based backend to work with IoTDash.

Regarding the pseudo-protocol - that's all of it. IOTDash and the client side implementations will make heavy use of it. To understand how, let's start from the devices-perspective.

Discovery, Events. Sensors and Commanding Implementation

Well, for all of this "magical" stuff, good old sockets and UDP come into the game. Even new discovery models including IoT gateways use the same established techniques to get a hold of your devices. It is a solid and fast way to discover devices on every platform that supports sockets out-of-the-box.

The NetMF Implementation

Implementing Discovery, Events, Sensors and Commanding on NetMF based devices is mainly done using this two classes:

  • UdpSocketServer - Handles the basic system parts like SysInfo, ListEvents and receives commands from IoTDash (or any other app) that are executed on the device, and it pushes event-data to IoTDash (or any other app), that is able to handle the event-data
  • UdpGrain - Takes care of advertising the existence of the device on the network (UDP broadcast)

In order to use this two classes successfully you should perform the following tasks within your NetMF app:

  • Establish a network connection
  • Set the current time for the device
  • Create and supply a discovery-configuration based on the interface IDeviceDiscoveryConfiguration
  • Pass that configuration to an instance of the UdpSocketServer-class
  • Bind to the DataReceived event of the UdpSocketServer class-instance (to receive commands) and impmlement to which commands to listen
  • New-up a UdpGrain instance by setting the interval (in milliseconds) and the device-type, as well as if the device is using a secure connection or an insecure connection
  • Start the Grain-Timer
  • Start the UDP-Server-Timer

UdpServer and UdpGrain are defined in an assembly called EasyDiscovery.  To change the list of possible commands (just to avoid every kind of command being accepted),

I hardcoded some commands into the UdpServer class. Please change the lines 122 - 130 within the EasyDiscovery project (file:UdpServer.cs) to allow only a specific set of commands for your devices or to allow all kinds of commands.

There is another hard-coded value, the network-address that is used to send-out broadcast-messages to. Change the address in line 43 within the same file to make it suitable for your needs.

Change this address also within the file UdpGrain.cs in line 27.

To not make this article a book, let's look at the source of Program.cs, I leave it up to you to discover the rest of the classes:

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using DeviceGrain.NetworkTools;
using EasyDiscovery;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using Microsoft.SPOT.Net.NetworkInformation;
using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using Gadgeteer.Modules.GHIElectronics;

namespace DeviceGrain
{
    public partial class Program
    {
      
        private const int ListenPort = 11000;

        private static UdpGrain _grain;
       
        private static UdpSocketServer _server;

        private static GT.Timer _isAliveTimer;

        private static GT.Timer _fakeSensorTimer;

        private static Random r;

        void ProgramStarted()
        {
          
            Debug.Print("Program Started");

            r = new Random();

            _server = new UdpSocketServer();

            _server.SetConfiguration(new DiscoveryConfiguration());
          
            _server.DataReceived += _server_DataReceived;

            InitNetwork();

            var currentTime = TimeSync.GetNtpTime("europe.pool.ntp.org", 1);

            Utility.SetLocalTime(currentTime);

            //Set the device type and connection type
            _grain = new UdpGrain(1000, "0x01:NOSSL");
          
            _grain.StartTimer();

            _server.Start();

            _isAliveTimer = new GT.Timer(3000);
            _isAliveTimer.Tick += _isAliveTimer_Tick;
            _isAliveTimer.Start();

            _fakeSensorTimer = new GT.Timer(1000);
            _fakeSensorTimer.Tick += _fakeSensorTimer_Tick;
            _fakeSensorTimer.Start();


            #region testevents

            //Thread t = new Thread(new ThreadStart(()=>{

            //        for(int i=1; i<50;i++)
            //        {

            //            _server.PushEvent("event" + i + "from t1","somedata");

            //        }

            //}));

            //t.Start();

            //Thread t2 = new Thread(new ThreadStart(() =>
            //{

            //    for (int i = 1; i < 100; i++)
            //    {

            //        _server.PushEvent("event" + i + "from t2", "somedata");

            //    }

            //}));

            //t2.Start();

            //Thread t3 = new Thread(new ThreadStart(() =>
            //{

            //    for (int i = 1; i < 80; i++)
            //    {

            //        _server.PushEvent("event" + i + "from t3", "somedata");

            //    }

            //}));

            //t3.Start();

            #endregion
        }

        void _fakeSensorTimer_Tick(GT.Timer timer)
        {
            _server.PushEvent("E02", r.Next(1300).ToString());
            _server.PushEvent("E01", r.Next(100).ToString() + " C");
        }

       
        void _isAliveTimer_Tick(GT.Timer timer)
        {
            _server.PushEvent("E00","1");
        }

        void _server_DataReceived(object sender, DataReceivedEventArgs e)
        {
            //TODO: Check for future release
            //Stop broadcasting, we have a commander now.
            //if (!_timerStopped)
            //{
            //    grain.StopTimer();
            //    _timerStopped = true;
            //}

            var command = new string(Encoding.UTF8.GetChars(e.Data));

            switch (command)
            {

                case "blink":
                    multicolorLED.BlinkRepeatedly(GT.Color.Blue, new TimeSpan(0, 0, 5), GT.Color.Red, new TimeSpan(0, 0, 5));
                    break;
                case "blinkgreen":
                    multicolorLED.BlinkRepeatedly(GT.Color.Green, new TimeSpan(0, 0, 5), GT.Color.Red, new TimeSpan(0, 0, 5));
                    break;
                case "blinkfast":
                    multicolorLED.BlinkRepeatedly(GT.Color.Blue, new TimeSpan(0, 0, 1), GT.Color.Red, new TimeSpan(0, 0, 1));
                    break;
                case "stopblink":
                    multicolorLED.TurnOff();
                    break;
                case "stopblinkfast":
                    multicolorLED.TurnOff();
                    break;

            }
        }

       
        private void InitNetwork()
        {
            var ethernet = ethernetJ11D.NetworkInterface;
           
            ethernet.Open();
            ethernet.EnableDhcp();
            ethernet.EnableDynamicDns();

           while(ethernet.IPAddress == "0.0.0.0")
           {
               Thread.Sleep(100);
           }

           Debug.Print("Network ok!");
          
        }
    }

  
}

 

Within this short piece of code you can see all the required steps that are necessary to start the UDPServer, receive commands, send out events and how to start the discovery socket.

Here is a list of device-types that IoTDash is able to understand:

  • 0x01 - GHI Fez Spider
  • 0x02 - GHI Raptor
  • 0x03 - Intel Galileo Gen 1
  • 0x04 - Intel Galileo Gen 2
  • 0x05 - Intel Edison
  • 0x06 - Raspberry PI B+

You use the hex-values within the DeviceGrain constructor like this to define the device type and if it is a secure or unsecured connection:

  • "0x01:NOSSL" - GHI Fez Spider, unsecure connection
  • "0x06:SSL" - Raspberry PI B+, Secure connection

IoTDash implements an enum called DeviceTypes that will recognize the device-type by the short hex-code and it will load the required device image. You can simply modifly this enum to suite your needs.

Currently the NetMF-based implementation does not support SSL, because I could not figure out, why I am unable to authtenticate the SSL-Server as a secure-server using NetMF 4.3 (QFE1). The SSL Server implementation is available within the source, but is not used. If you know how this can be done, using certificates, please let me know about it.

The Node.js based Implementation

Implementing socket-based scripts in Node.js is pretty straight-forward. The same is valid for secure-connections. You can simply copy and paste this code to any device that runs node. The certificates are attached within the source-code files, so that you can test the implementation on your own. Adjust everything according to your needs and replace the subnet-address for the broadcast-client, you can find that line right after the comment that contains the words "SSL CLIENT SPEC":

var dgram = require('dgram');

var tls = require('tls');

var fs = require('fs');

var events = require('events');

var eventChannel = new events.EventEmitter();

var eventSocket;

var eventInterval;

var eventIntervalAlive;


/****STANDARD PORTS********/


var DEFAULT_EVENT_PORT = 8090;

/*****SSL CLIENT SPEC********/
var message = new Buffer("0x06:SSL");

var client = dgram.createSocket("udp4");

client.bind(function(){client.setBroadcast(true);});

var broadcastInterval = setInterval(function(){
client.send(message,0,message.length,9100,"192.168.178.255");
},1000);


/***** COMMAND CONFIGURATION ****/
var commands ={Commands:[
        "blinkfast",
        "stopblink",
        "stopblinkfast",
	"onlyongalileo"]}
    ;

/****** HOST-PROPERTIES (SYSINFO)******/
var hostProps = 
     {
        "Name": "Raspberry PI B+",
        "Backend": "Node.js",
	"GetCommands": "ListCommands",
	"GetEvents": "ListEvents",
	"GetSensors": "ListSensors",
	"GetSysInfo": "SysInfo"

    };


/****** EVENTS ************/
var events = {  "Events":
	
	
		[{	"EventId":"E00",
			"EventName":"ALIVE",
			"EventDataType":"INT",
			"SensorName":"S00"
		},
		{
                        "EventId":"E0115",
                        "EventName":"GETTEMP",
                        "EventDataType":"DEC",
                        "SensorName":"S01"
                },
		{
                        "EventId":"E0215",
                        "EventName":"GETMOIST",
                        "EventDataType":"DEC",
                        "SensorName":"S02"
                }]
	};


/********** SENSORS *********/
var sensors = 
		{"Sensors":[
			{
			    "SensorId":"S00",
			    "SensorType":"DASS"
			    		
			},

			 {
                            "SensorId":"S01",
                            "SensorType":"TEMP"

                        },

			 {
                            "SensorId":"S02",
                            "SensorType":"MOIST"

                        }
		]};


/******* EVENT, EVENTS*********/
eventChannel.addListener('sensorEvent',function(eventData){

		//console.log("EventSocket");

		if(eventSocket !== 'undefined' && eventSocket !== null)
		{
				eventSocket.write(eventData.eventToPush+'~'+eventData.data+"<eof>");
		}

});

StartSecureListener();
StartEventPropagator();

var eventLoopStarted = false;

function StartEventPropagator()
{

	try{
	
	var options = {
		key: fs.readFileSync('./certs/key.pem'),
		
		cert: fs.readFileSync('./certs/cert.pem')
	};

	console.log("event l start");
	tls.createServer(options, function(s){
	
		eventSocket = s;
		

		s.on('error', function(ex){
			//console.log(ex);
		});

		if(eventLoopStarted === false)
		{
			StartEventLoopDeviceAliveSignal();
			StartEventLoopForTestSensors();
			eventLoopStarted = true;
		}



	}).listen(DEFAULT_EVENT_PORT);
	}catch(ex)
	{
		console.log(ex);
	}

	

}


function StartEventLoopForTestSensors()
{
	eventInterval  = setInterval(function(){
		
	 	//Sending a simple moisture value event	
		var humDataString = Math.round(Math.random()* ((1000-100)+100));

		eventChannel.emit('sensorEvent',{eventToPush:'E0215', data:humDataString});
		
		var tempDataString = Math.round(Math.random() * ((100-1)+1)) + ' C';
		//Sending out temperature between 1 and 100 C
		eventChannel.emit('sensorEvent',{eventToPush:'E0115', data:tempDataString});



	},1000);

}


function StartEventLoopDeviceAliveSignal()
{
	eventIntervalAlive  = setInterval(function(){
		
		//That's enough to indicate that we are still alive here...
		eventChannel.emit('sensorEvent',{eventToPush:'E00', data:'1'});


	},3000);

}






function StartSecureListener()
{

	var options = {
		key: fs.readFileSync('./certs/key.pem'),
		
		cert: fs.readFileSync('./certs/cert.pem')
	};

	tls.createServer(options, function(s){

		//Stop broadcasting, we have been discovered
		//clearInterval(broadcastInterval);

		var sysInfo = JSON.stringify(hostProps);
		var cmdList = JSON.stringify(commands);
		var sensorList = JSON.stringify(sensors);
		var eventList = JSON.stringify(events);
		

		s.on('data',function(data){
			var cmdData = data.toString('utf-8');
			console.log(cmdData);			
			//check, what command we have received
			switch(cmdData)
			{
			  case 'ListCommands':
				console.log("Command List Requested");
				s.write(cmdList+"<eof>");
				break;
			  case 'ListSensors':
				console.log("Sensor List Requested");
				s.write(sensorList+"<eof>");	
				break;
			  case 'ListEvents':
				console.log("Event List Requested");
				s.write(eventList+"<eof>");
				break;
			  case 'blink':
				console.log("blink command called");
				break;
			  case 'stopblink':
				console.log("blinkstop command called");
				break;
			  case  'blinkfast':
				console.log("blinkfast command called");
				break;
			  case 'stopblinkfast':
				console.log("stopblinkfast command called");
				break;
			  case 'SysInfo':
				console.log("SysInfo requested.");
				s.write(sysInfo+"<eof>");
				break;
			 case "onlyongalileo":
				console.log("Only called");
				break;
			}

		});

		s.on('error', function(ex){
			console.log(ex);
		});

	}).listen(3340);

}

</eof></eof></eof></eof></eof>

Would be happy to get some feedback on this code from real Node.js experts.

IoTDash - Base Architecture and Communication-Channels

One article is simply not enough to explain all parts of IoTDash in detail. Therefore I am going to explain the most important parts of IoTDash and the communication-channels.

IoTDash Base Architecture

IoTDash references two main projects:

  • DeviceDiscovery - Used to discover devices on the network
  • CommChannels - This is where all the communication-channels are implemented

The DeviceDiscovery project implements the following parts and functions:

  • The model classes (POCO's) for Events, Commands, Sensors, DeviceTypes and SysFeatures
  • EventReceivedEventArgs - Contains the data for a specific sensor
  • DeviceAddedEventArgs - Delivered when a new device was discovered
  • DeviceListener - Responsible to listen for devices (secure and unsecure ones, used during discovery), Creates the needed SSL and non-SSL command clients
  • SSLClient - Secure command and event-listening client
  • NoSSLClient - Unsecure command and event-listening client

This code makes heavy use of threading  to allow IoTDash to operate smoothly, even on smaller tablets. The rest simple and plain MVVM using MVVM-Light.

Communication-Channels Implementation

Communication-Channels represent the data-pipelines to different kind of data-stores and services. Using Comm-Channels, IoTDash has the ability to transport the sensor-data coming from IoT-Devices to the outside-world or to your local SQL Server. Like a gateway.

This version of IoTDash contains channels to SignalR (Can be used with Azure-Websites), Azure Tables, Azure Event Hubs and SQL Server.

All of the channels are based on one interface, ICommChannel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CommChannels
{
    public interface ICommChannel<T,TU>
    {
        Task<bool> Connect();

        Task<bool> Disconnect();

        Task<bool> SendData(T data);

        Task<TU> SetConfig(TU config);

        Task<TU> LoadConfig();

        Task<bool> SaveConfig(TU config);

    }
}

Here is the sample implementation that is used to open a pipe to Azure Event Hubs:

using CommChannels.Config;
using Microsoft.ServiceBus.Messaging;
using Newtonsoft.Json;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CommChannels.EventHubChannel
{

    public class EventHubCommChannel : ICommChannel<EventHubChannelModel, EventHubConfig>
    {
        private EventHubConfig _configuration;

        private readonly AsyncLock _mutex = new AsyncLock();

        public EventHubConfig Configuration
        {
            get
            {
                return _configuration;
            }

            set
            {
                _configuration = value;
            }
        }

        private EventHubClient _client;

        public  Task<bool> Connect()
        {
            var tcs = new TaskCompletionSource<bool>();

            try
            {
                _client = EventHubClient.CreateFromConnectionString(_configuration.EventHubConnectionString,_configuration.EventHubName);

                tcs.SetResult(true);
            }

            catch (Exception ex)
            {
                tcs.SetResult(false);
            }

            return tcs.Task;
        }

        public async Task<bool> Disconnect()
        {

            try
            {
                await _client.CloseAsync();

                return true;

            }

            catch (Exception ex)
            {
                return false;
            }

        }

        public async Task<bool> SendData(EventHubChannelModel data)
        {
            using (await _mutex.LockAsync())
            {
                try
                {

                    var eventData = new EventData(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data)));
                    eventData.PartitionKey = "0";
                    eventData.Properties.Add("Type", "DeviceMessage");
                    await _client.SendAsync(eventData);

                    return true;
                }

                catch (Exception ex)
                {
                    return false;
                }
            }
        }

        public async Task<EventHubConfig> SetConfig(EventHubConfig config)
        {
            return await Task.Run<EventHubConfig>(async () =>
            {

                await SaveConfig(config);

                return config;

            });
        }

        public async Task<EventHubConfig> LoadConfig()
        {
            return await Task.Run<EventHubConfig>(() =>
            {
                if (File.Exists(@".\ConfigFiles\EventHubConfig.json"))
                {
                    var configData = File.ReadAllText(@".\ConfigFiles\EventHubConfig.json");
                    var config = JsonConvert.DeserializeObject<EventHubConfig>(configData);
                }

                return new EventHubConfig();
            });
        }

        public async Task<bool> SaveConfig(EventHubConfig config)
        {
            return await Task.Run<bool>(() =>
            {
                if (config == null)
                {
                    var configData = JsonConvert.SerializeObject(_configuration);

                    File.WriteAllText(@".\ConfigFiles\EventHubConfig.json", configData);

                    return true;
                }

                else
                {
                    var configData = JsonConvert.SerializeObject(config);

                    File.WriteAllText(@".\ConfigFiles\EventHubConfig.json", configData);

                    return true;
                }

            });
        }
    }
}

That's it! My contribution for the Microsoft-Azure contest on CodeProject! Thanks for reading :)

Credits for Components and Libraries Used

  • Syncfusion for sponsoring the WPF-Components used in IoTDash via the "Essential Studio Enterprise Edition Community License" - The binaries are NOT distributed with this project. To compile the project please claim a free license. You can do it here.
  • Oystein Bjorke and his PropertyTools for WPF. Check them out here
  • The excellent DI-Framework Ninject (used for the ViewModelLocator). Check it out here.
  • Stephen Cleary and his excellent AsyncEx-Library. Check it out here.
  • Laurent Bugnioin's MVVM-Light Toolkit. Check it out here.
  • Last but not least of course the excellent Azure libraries developed by the Azure-Team. Check them out here.