IoT & Microsoft Dynamics AX – Part 2

In the second part of this series I will focus on building the base station and enabling the data transfer to Microsoft Azure Event Hubs.

For the base station we will use a Freetronics EtherTen module, basically an Arduino Uno with onboard Ethernet. The setup is very simple with the NRF905 connected to the MISO, MOSI and SCK ports on digital pins 11, 12 and 13.

The NRF905 uses a large number of digital ports (9 in total) plus GND and 3.3V leaving little remaining on the Uno, so if your project requires more you might want to switch to a Mega board instead. As before, the onboard Ethernet blocks digital port 10, so I’ve re-arranged the wiring to accommodate this.

In this series I am not going to go into depth with configuring the boards or setting up the various portions in Azure – there are many good articles written to cover this, and nothing I do here in this project can improve on those. My focus is on prototyping end to end projects and exploring whether IoT solves actual business problems. I’m an entrepreneur, and unless I can provide real business value or solve serious customer pain with IoT, it remains nothing more than a cool technology or a weekend hobby.

So let’s dive right into it!

Below is the wired-up Arduino connected to Ethernet and powered by a 9V rechargeable battery. One of the biggest issues I faced early on when working with these controllers is voltage drop when using non-rechargeable batteries, resulting in flaky communications with the RF modules, particularly when I used the NRF24L01 module, and a standard recommendation is to set a capacitator between VCC and GND on the RF module, typically 100nF to 5uF. My current setup with the NRF905 works just fine so it is not required.

Station2

The logic for the base station is pretty straightforward and involves the following process:

  • Setup the NRF905 and Ethernet by using a fixed MAC address.
  • Establish a connection to the worker role service running in Azure. Recall from Part 1 that due to the UNO not supporting HTTPS, we will have to route via a worker role.
  • Once connected to Azure, wait for incoming messages from a remote monitor.
  • As a message comes in, strip out the data we need and pass this to the Azure worker role for routing and send back an ACK signal to the monitor.

The C source code is shown below. There are several important things missing in the code, but it is a PROTOTYPE after all:

  • Code security – we are not checking for any buffer overflow.
  • Encryption – data is sent clear between devices and to the Azure worker role.
  • Managing dropped Ethernet connections.
  • No authentication scheme between the base station and the Azure worker role.
#include <nRF905.h>
#include <SPI.h>
#include <Ethernet.h>

#define RXADDR {0x58, 0x6F, 0x2E, 0x10} // Address of this station
#define TXADDR {0xFE, 0x4C, 0xA6, 0xE5} // Address of monitor device
byte mac[] = { 0x90, 0xA2, 0xDA, 0x0D, 0xBC, 0xAE };
EthernetClient client;
bool connected = false;

void setup()
{
	Serial.begin(9600);

	nRF905_init();
	byte addr[] = RXADDR;
	nRF905_setRXAddress(addr);
	nRF905_receive();
	Serial.println("Communications up...");

	Ethernet.begin(mac);
	Serial.println("Ethernet up...");
	Serial.println("Base station running...");
}

void loop()
{
	if (!connected)
	{
		Serial.println("Trying to connect");
		char* host = "finitemindsserver.cloudapp.net"; // your Azure service or event hub details go here
		client.setTimeout(10000);
		connected = (client.connect(host, 10100) == 1);
		if (connected) {
			Serial.println("Connected to Azure...");
		}
		else
		{
			Serial.println("Connection unsuccessful...");
			client.stop();
		}
	}
	if (connected)
	{
		// get latest data from remote device
		byte buffer[NRF905_MAX_PAYLOAD];
		while (!nRF905_getData(buffer, sizeof(buffer)));

		// got data inside buffer, check for packet signature + worker id + light measurement
		if ((buffer[0] == 'G') && (buffer[1] == 'V'))
		{
			// extract what we need and send to Azure worker role
			byte sendbuffer[5];
			for (int i = 0; i < 5; i++)
			{
				sendbuffer[i] = buffer[i + 2];
			}
			client.write(sendbuffer, sizeof(sendbuffer));
		}
		
		// send back as an echo to ACK
		byte addr[] = TXADDR;
		nRF905_setTXAddress(addr);
		nRF905_setData(buffer, sizeof(buffer));
		while (!nRF905_send());
		nRF905_receive();
	}
}

 

At this point we should be good to go, and can move on to building our worker role in Azure. I’ll use C# for this and the code is below.

Again, very simple and it merely listens for an incoming TCP connection on port 10100, and once accepted proceeds to log incoming messages as events in our Azure Event Hub.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.Storage;
using System.Net.Sockets;
using System.IO;
using System.Globalization;
using Microsoft.ServiceBus;
using Microsoft.ServiceBus.Messaging;
using Newtonsoft.Json;
using System.Text;
using System.Runtime.Serialization;

namespace FiniteWorker
{
    public class WorkerRole : RoleEntryPoint
    {
        private string eventHubName = "finitehub";
        private string connectionString = "Endpoint=sb://finiteminds.servicebus.windows.net/;SharedAccessKeyName=SendRule;SharedAccessKey=<your key here>";
        private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
        private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false);
        private AutoResetEvent connectionWaitHandle = new AutoResetEvent(false);

        public override void Run()
        {
            TcpListener listener = null;

            try
            {
                listener = new TcpListener(RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["FiniteEndPoint"].IPEndpoint);
                listener.ExclusiveAddressUse = false;
                listener.Start();
            }
            catch (SocketException)
            {
                Trace.Write("Finite server could not start.", "Error");
                return;
            }
            while (true)
            {
                 IAsyncResult result = listener.BeginAcceptTcpClient(HandleAsyncConnection, listener); 
                connectionWaitHandle.WaitOne();
            }
        }

        private void HandleAsyncConnection(IAsyncResult result)
        {
            var eventHubClient = EventHubClient.CreateFromConnectionString(connectionString, eventHubName);
            TcpListener listener = (TcpListener)result.AsyncState;
            TcpClient client = listener.EndAcceptTcpClient(result);
            connectionWaitHandle.Set();

            while (client.Connected)
            {
                NetworkStream netStream = client.GetStream();
                StreamReader reader = new StreamReader(netStream);

                byte[] buffer = new byte[5];
                int bytesRead = client.Client.Receive(buffer,0 ,5, SocketFlags.None);
                if (bytesRead == 5) // worker id + light sensor reading
                {
                    byte[] bWorker = new byte[3];
                    byte[] bLight = new byte[2];
                    bWorker[0] = buffer[0];
                    bWorker[1] = buffer[1];
                    bWorker[2] = buffer[2];
                    bLight[0] = buffer[3];
                    bLight[1] = buffer[4];

                    DeviceMessage msg = new DeviceMessage();
                    try
                    {
                        msg.Worker = Encoding.UTF8.GetString(bWorker, 0, 3);
                        msg.Light = Convert.ToInt32(Encoding.UTF8.GetString(bLight, 0, 2));
                    }
                    catch
                    {
                        // log exception here and handle it
                    }
                    string json = Newtonsoft.Json.JsonConvert.SerializeObject(msg);
                    eventHubClient.Send(new EventData(Encoding.UTF8.GetBytes(json)));
                }
            }
            client.Close();
        }

        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections
            ServicePointManager.DefaultConnectionLimit = 12;
            bool result = base.OnStart();
            Trace.TraceInformation("FiniteWorker has been started");
            return result;
        }

        public override void OnStop()
        {
            Trace.TraceInformation("FiniteWorker is stopping");
            this.cancellationTokenSource.Cancel();
            this.runCompleteEvent.WaitOne();
            base.OnStop();
            Trace.TraceInformation("FiniteWorker has stopped");
        }

        private async Task RunAsync(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                Trace.TraceInformation("Working");
                await Task.Delay(1000);
            }
        }
    }

    [DataContract]
    public class DeviceMessage
    {
        [DataMember]
        public string Worker { get; set; }
        [DataMember]
        public int Light { get; set; }
    }
}

 

Let’s connect both Arduino’s to our USB ports so we can watch for messages via the serial interface. The screenshot below shows both devices connected with the base station on COM4 and the monitor on COM5.

VisualStudioView

Both are communicating fine and messages are being sent the Azure worker role as shown below.

AzureEventHub

So let’s go ahead and consume these messages by creating an Azure stream job. The input is of course our Event Hub, and I’ve set the output to be Microsoft Power BI. The query is a simple “SELECT FROM INTO” literally passing everything from the Event Hub directly to Power BI as output with no filtering.

IoTStream

The setup in Microsoft Power BI is relatively simple as shown below. Note the spike in the bottom chart at 3:52AM – this was detected as I switched on additional lighting, and it simply shows us the light sensor on the monitor is working as expected.

PowerBISetup

Once configured we can go ahead and view the report on our device of choice, in my case an Apple iPad as shown below.

TabletView

Nothing groundbreaking so far, but the objective of this project is to get data from a remote device eventually into Microsoft Dynamics AX7, and my next post in this series will focus on achieving that.

Advertisements