Dynamics AX RFID using IoT

Don’t you hate it when you need to track someone down in an office building for an urgent message, or to sign some paperwork and you have no idea where they are? In a meeting perhaps, phone switched on silent? In the kitchen taking a break?

So here is a quick and easy worker locator using RFID swipe cards with an IoT board that displays your office layout in Dynamics AX, and pin-points where you can find Waldo right now.

For this project we’ll use a $20 Adafruit HUZZAH ESP8266/WiFi board with an MFC522 RFID reader. Mount the reader in a small plastic casing as shown below. The signal is strong enough to read access cards through the plastic wall.

We’ll add a LiPo rechargeable battery to make this portable as well. Just in case you need to mount one of these as and when required.

IMG_0182

Next we’ll solder the Adafruit board onto some PCB and add a red and green LED to signal “success” or “failure” when a card is touched to the box. The PCB is really just required to add resistors between VCC and the LED’s.

I like reusing stuff between projects so again, mounting the IoT board with headers onto the PCB makes sense. The completed PCB setup is shown below. The edges of the PCB have been trimmed to fit into the box with the rest, and that measures 8cm x 5cm so perfect to mount on a wall.

IMG_0184

Once the RFID reader has been connected and soldered onto the PCB we can stack all of it safely into the box as seen below. The LiPo battery can be recharged via USB and we can get a lot of usage out of it between charges, by putting the ESP8266 WiFi chip into deep sleep mode as often as possible.

IMG_0186

We’ll drill two small holes for the LED’s at the top, connect the battery and make another hole for a USB charger connection. All sorted the finished product is seen below ready for mounting.

IMG_0189

Coding-wise, I’ve used C through the Arduino IDE to make the whole project work. The code for this is below.

#include <SPI.h>
#include <MFRC522.h>
#include <ESP8266WiFi.h>
 
#define RST_PIN         15
#define SS_PIN          2
#define LED_GREEN       4
#define LED_RED         5
 
const char* tempssid = "TempAP"; // use a mobile phone to setup AP using IoTLink.net
const char* temppass = "TempPass";
const char* iotlink = "www.iotlink.net";
bool routed = false;
 
MFRC522 mfrc522(SS_PIN, RST_PIN);
 
void setup() {
  Serial1.begin(115200);
  while(!Serial1){}
 
  Serial.begin(9600);
  SPI.begin();
  mfrc522.PCD_Init();
  pinMode(LED_GREEN, OUTPUT);
  pinMode(LED_RED, OUTPUT);
  digitalWrite(LED_GREEN, LOW);
  digitalWrite(LED_RED, LOW);
 
  WiFi.begin(tempssid, temppass);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
}
 
void loop()
{
  if (!routed)
  {
    route();
  }
 
  MFRC522::MIFARE_Key key; // default to FFFFFFFFFFFF
  key.keyByte[0] = 0xFF;
  key.keyByte[1] = 0xFF;
  key.keyByte[2] = 0xFF;
  key.keyByte[3] = 0xFF;
  key.keyByte[4] = 0xFF;
  key.keyByte[5] = 0xFF;
 
  // Loop until card is presented
  if (!mfrc522.PICC_IsNewCardPresent()) {
    return;
  }
 
  if (!mfrc522.PICC_ReadCardSerial())   
  {
    digitalWrite(LED_RED, HIGH);
    delay(1000);
    return;
  }
 
  byte readbuffer1[18];
  byte readbuffer2[18];
  byte block;
  MFRC522::StatusCode status;
  byte len;
  byte sizeread = sizeof(readbuffer1);
 
  block = 0;
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block, &key, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    // failed
    digitalWrite(LED_RED, HIGH);
    delay(1000);
    digitalWrite(LED_RED, LOW);
    return;
  }
  else
  {
    for (int i = 0; i < 18; i++)
    {
      readbuffer1[i] = 0x00;
      readbuffer2[i] = 0x00;
    }
 
    // read worker name or ID from card
    status = mfrc522.MIFARE_Read(1, readbuffer1, &sizeread);
    if (status != MFRC522::STATUS_OK)
    {
      // read failed
      digitalWrite(LED_RED, HIGH);
      delay(1000);
      digitalWrite(LED_RED, LOW);
      return;
    }
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
  }
 
  // success
  String workername = String((char *)readbuffer1);
  digitalWrite(LED_GREEN, HIGH);
  delay(1000);
  digitalWrite(LED_GREEN, LOW);
 
  // send to ThingSpeak
  const char* host = "api.thingspeak.com";
  const char* thingspeak_key = "your api key";
 
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort))
  {
    return;
  }
 
  String url = "/update?key=";
  url += thingspeak_key;
  url += "&field1=";
  url += workername;
 
  client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
  delay(10);
  while(client.available()){
    String line = client.readStringUntil('\r');
  }
}
 
void route()
{
  WiFiClient client;
 
  if (!client.connect(iotlink, 80))
  {
    Serial.println("IoTLink connection failed");
    return;
  }
 
  // uniquely identify our device using the chip id.
  // this id needs to be loaded into IoTLink and mapped to an AP by the customer
  String url = "/route?deviceid=" + String(ESP.getChipId());
  client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + iotlink + "\r\n" + "Connection: close\r\n\r\n");
  unsigned long timeout = millis();
  while (client.available() == 0) {
    if (millis() - timeout > 5000) {
      Serial.println("IoTLink Timeout !");
      client.stop();
      return;
    }
  }
  String response = "";
  while(client.available()){
    response = client.readStringUntil('\r'); 
  }
  response.remove(0,1); // gobble NL
  String ssidx = getValue(response,',',0);
  String passx = getValue(response,',',1);
  char ssidnew[32] = {0};
  char passnew[32] = {0};
  ssidx.toCharArray(ssidnew, 32);
  passx.toCharArray(passnew, 32);
 
  WiFi.disconnect();
  delay(4000);
  WiFi.begin((char *)ssidnew, (char *)passnew);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  routed = true;
  // optionally store retrieved AP information into EEPROM in case of device reset
  // this avoids having to use a temporary AP again to route to IoTLink
}
 
// Helper
String getValue(String data, char separator, int index)
{
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data.length()-1;
 
  for(int i=0; i<=maxIndex && found<=index; i++)
  {
    if(data.charAt(i)==separator || i==maxIndex)
    {
        found++;
        strIndex[0] = strIndex[1]+1;
        strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }
  return found>index ? data.substring(strIndex[0], strIndex[1]) : "";
}
 

 

First, we use a temporary AP and IoTLink (www.iotlink.net) to connect to the assigned AP for internet access. Once that is routed and connected, we wait for a card swipe. We read the worker details off the card, and send this to ThingSpeak via our WiFi connection.

ThingSpeak supports multiple channels with up to 8 fields per channel. For this example, I used a single channel and assumed each field will correspond to an office or room. Since that is limited, you might want to create a channel per location where you want to mount an RFID reader and just use one single field per channel. This is a prototype project so I took the easy way out.

Once we are satisfied that the electronics work, and swiping a card generates data in ThingSpeak, we can switch to AX 7.

For AX, I created a simple form and an extended control. In the HTML of the control, I load the office layout graphic (directly from my friends at EDrawSoft who kindly gave me permission to use it) and then get the latest data from my ThingSpeak channel using JavaScript.

I can go through each channel and field to determine who has swiped where in the building, and in this example we’ll just look for worker ‘VOS’. Once found, we can highlight where they last swiped in red around that office or room, and display their name at the top. The HTML/JavaScript for AX is shown below.

src="/resources/scripts/FBXRFIDControl.js">
id="FBXRFIDControl">
/>
style="border-width: 1px; border-style: solid; width: 100%; height: 100%; float: left;"> id="thecanvas">
<br /><br /> type="text/javascript"> var clickX = new Array(); var clickY = new Array(); var clickDrag = new Array(); var paint; function InitForm() { var thecanvas = document.getElementById('thecanvas'); thecanvas.width = 700; thecanvas.height = 600; var thecontext = thecanvas.getContext("2d"); var theImage = new Image(); theImage.onload = function () { thecontext.drawImage(this, 0, 0); } theImage.src = "https://www.edrawsoft.com/images/examples/office%20layout%20sample.png"; $.getJSON('https://www.thingspeak.com/channels/yourchannel/feed/last.json?callback=?&offset=0&location=true;key=yourkey', function (data) { var worker = data.field1; if (worker != '') { // someone in office 1, display who it is and highlight this office as 'occupied' var thecanvas = document.getElementById('thecanvas'); var thecontext = thecanvas.getContext("2d"); thecontext.fillStyle = "red"; thecontext.font = "bold 10px Arial"; thecontext.fillText(worker, 30, 30); thecontext.strokeStyle = "Red"; thecontext.lineWidth = 6; thecontext.rect(10, 15, 235, 175); thecontext.stroke(); } }); } </div>

 

Running in AX produces the results shown below. The code needs some work to add drop-downs for selecting worker, or perhaps clicking on a room will display the names of everyone currently swiped into that room. There are many ways to improve on this.

AX7

A nice simple project to combine RFID, IoT and Dynamics AX into an easy to use little project.

Advertisements

IoT Health & Safety in Dynamics AX 7

This is a quick project I put together to explore using small-size IoT modules for practical work, say Health & Safety or Equipment monitoring and reporting. For this post I’ll use the super-compact Cactus Rev 2 which gives me multiple I/O ports with WiFi built-in via an ESP8266 and I will use that to transmit three sensor values to Dynamics AX 7 via ThingSpeak.

The Cactus Rev 2 module is an amazing little device that measures less than 4cm by 2cm and is half a centimeter thick. It can be powered via USB or RAW and runs off <4v which is important as I want to power this using a LiPo battery to make it portable. The Cactus is pretty close to being the best possible module to use for building “wearable” IoT and can be powered using two coin batteries, with the ESP8266 being disabled and enabled as required to conserve power.

To make things a bit more practical, I’ll add a DHT11 temperature and humidity sensor, as well as a light sensor. I can also add an MQ9 sensor to detect various dangerous gasses but since that requires 5v it will need a booster added to the circuit so I’ll pass. It’s the idea that counts, right?

The completed circuit is shown below after soldering everything to a PCB and doing the required wiring, using 1 x Analog and 1 x Digital port all up, so leaving plenty of room for other sensors if required. I tried to keep things small and this measures 5cm x 4cm and about 1cm thick when completed. Keep in mind that sending this to production will drastically reduce the size, but this is only a prototype.

IMG_0173

I’m running this off USB to test, but ran the wiring (top-left) from RAW and GND to the back of the circuit where I can mound a rechargeable LiPo battery, and this will recharge  automatically every time I connect the USB cable. So it’s portable, virtually wearable and can be recharged at your desk.

We’ll use WiFi via the onboard ESP8266 and for that we’ll need an Access Point (AP) and obviously SSID and PASS. I could have opted to use an Arduino Nano with NRF905 instead as well. While it’s a prototype, we want to be production-ready so the sketch includes WiFi routing via a vendor/customer IoT Directory available at www.iotlink.net and I have coded this into the sketch. This means we’ll just need a temporary hardcoded AP during installation (or search for open AP) and IoTLink will take care of the rest.

The sketch code (in C) is shown below, pretty straightforward. We connect to a temporary AP, then using that and IoTLink find the correct customer AP settings in the device directory and re-route. Once connected we can start measuring sensor values and send  that to ThingSpeak to display in a dashboard.

#include <espduino.h>
#include <rest.h>
#include "DHT.h"
 
#define DHTPIN 15
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

#define PIN_ENABLE_ESP 13

ESP esp(&Serial1, &Serial, PIN_ENABLE_ESP);
REST rest(&esp);
boolean wifiConnected = false;

//IoTLink
boolean routed = false;
#define DEVICE_ID "DEV001" // This unique device ID
#define SSID  "TempSSID" // Default AP used for configuation
#define PASS  "TempPASS" 

void wifiCb(void* response)
{
  uint32_t status;
  RESPONSE res(response);

  if(res.getArgc() == 1) 
  {
    res.popArgs((uint8_t*)&status, 4);
    if(status == STATION_GOT_IP) 
    {
      wifiConnected = true;
    } else {
      wifiConnected = false;
    }  
  }
}

void setup() {
  Serial1.begin(19200);
  Serial.begin(19200);
  delay(1000);
  dht.begin();
  esp.enable();
  delay(500);
  esp.reset();
  delay(500);
  while(!esp.ready());
  if(!rest.begin("www.iotlink.net", 443, true)) // use SSL secure connection
  {
    while(1);
  } 
  esp.wifiCb.attach(&wifiCb);
  esp.wifiConnect(SSID, PASS); 
}

void loop() 
{
  esp.process();
  if(wifiConnected) 
  {
    if (!routed)
    {
      delay(1000);
      Route();
    }
    else
    {
      float h = dht.readHumidity();
      float t = dht.readTemperature();
      int l = analogRead(2);
      char response[266];
      char buff[64];
      char str_hum[6], str_temp[6], str_light[6];
      dtostrf(h, 4, 2, str_hum);
      dtostrf(t, 4, 2, str_temp);
      dtostrf(l, 6, 0, str_light);
      sprintf(buff, "/update?api_key=<your key here>&field1=%s&field2=%s&field3=%s", str_temp, str_hum, str_light);
      rest.get((const char*)buff);

      if(rest.getResponse(response, 266) == HTTP_STATUS_OK)
      {
      }
      delay(5000);
    }
  }
}

bool Route()
{
  char response[60]={','};
  char buff[50]={0};
  char ssid_new[20]={0};
  char pass_new[20]={0};

  sprintf(buff, "/route?deviceid=%s", DEVICE_ID);
  rest.get((const char*)buff);
  if(rest.getResponse(response, 60) == HTTP_STATUS_OK)
  {  
    String sr(response);
    String ssidx = getValue(sr,',',0);
    String passx = getValue(sr,',',1);
    ssidx.toCharArray(ssid_new,20);
    passx.toCharArray(pass_new,20);
    wifiConnected = false;
    routed = true;
    delay(500);
    esp.reset();
    delay(500);
    while(!esp.ready());
    if(!rest.begin("api.thingspeak.com"))
    {
      while(1);
    } 
    esp.wifiCb.attach(&wifiCb);
    esp.wifiConnect(ssid_new, pass_new);
    return true;
  }
  return false;
}

// Helper
String getValue(String data, char separator, int index)
{
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data.length()-1;

  for(int i=0; i<=maxIndex && found<=index; i++){
    if(data.charAt(i)==separator || i==maxIndex){
        found++;
        strIndex[0] = strIndex[1]+1;
        strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }
  return found>index ? data.substring(strIndex[0], strIndex[1]) : "";
}

 

I could have used Azure IoT Hubs and Power BI instead of ThingSpeak, but getting that working is time-consuming (compared to ThingSpeak), requires TSL from what I can gather and the Cactus just does not have the power nor storage capacity (it does support SSL though).

Running our circuit via the Arduino IDE in debug mode shows us it is all working nicely, as can be seen below:

IoTArduino

Dashboards are getting populated too, so the data is streaming in and we can move  this into Dynamics AX 7.

IoTThingSpeak

Say instead of wearable Health & Safety we wanted to use this to monitor a vital piece of equipment on the shop floor instead. For that we’ll create a simple dialog in AX, and add an extended control which will do the data fetching from ThingSpeak and display it on a factory layout diagram.

We start with a basic control, and then modify the HTML to include some JavaScript as shown below. All we are effectively doing is fetching the latest sensor readers from ThingSpeak, as reported by the IoT device. We request the latest sample, essentially.

One thing I am yet to find a workaround for in AX is the ability to utilise a JavaScript timer. This seems to be disabled somehow in AX. A timer would be useful to refresh all samples, say every 15 seconds or so. If someone has figured this out, please let me know.

src="/resources/scripts/FBXRFIDControl.js">
id="FBXRFIDControl">
/>
style="border-width: 1px; border-style: solid; width: 100%; height: 100%; float: left;"> id="thecanvas">
<br /><br /> type="text/javascript"> var clickX = new Array(); var clickY = new Array(); var clickDrag = new Array(); var paint; function InitForm() { var thecanvas = document.getElementById('thecanvas'); thecanvas.width = 700; thecanvas.height = 400; var thecontext = thecanvas.getContext("2d"); var theImage = new Image(); theImage.onload = function () { thecontext.drawImage(this, 0, 0); } theImage.src = "https://www.edrawsoft.com/templates/images/production-pfd.png"; $.getJSON('https://www.thingspeak.com/channels/yourchannel/feed/last.json?callback=?&offset=0&location=true;key=yourkey', function (data) { var temp = data.field1; var hum = data.field2; var light = data.field3; // can change font of text depending on whether light levels are sufficient // in turn, IoT device can adjust office lighting if levels are too high or too low var thecanvas = document.getElementById('thecanvas'); var thecontext = thecanvas.getContext("2d"); thecontext.fillStyle = "blue"; thecontext.font = "bold 10px Arial"; thecontext.fillText(temp + 'C ' + hum + '%', 490, 250); }); } </div>

 

Running this in AX produces the result shown below, with the sensor readings being displayed in blue next to the vital piece of equipment we are monitoring, in this case a motor.

IoTOHS

In terms of practicality it covers a number of potential use-cases:

  • Office environment monitoring
  • Health & Safety monitoring (toxic gasses, working temperature, low-light conditions, high humidity)
  • Add a vibration sensor to monitor potential equipment malfunction