Visualize 3D Models in D365 FinOps

In this short blog post I’m going to show you how to build a 3D extensible control using the Extensible Control Framework in Dynamics 365 FinOps (AX). This can come in handy for ISV’s working in the manufacturing or additive manufacturing space (3D Printing).

Being able to fully visualize and interact with 3D models of parts within D365 brings us one step close to having a full end to end ERP > 3D printing interface, which is a side project I am working on.

Extensible controls allows us to build self-contained visual controls that we can share and allow other developers to simply drop onto a form. There are basically 3 main parts to it, the HTML, optional JavaScript, and the X++ class for the control itself, which allows us to communicate between the web browser front-end and the X++ back-end side of things.

For this post I’ll focus on the STL file format, arguably the most popular of the 3D formats, and widely used by 3D printers. We’ll add some basic properties to the control, including the URL of the STL file we want to visualize, object color and control height and width. This can be extended further of course, but we’ll keep things simple for a start.

We’ll start with the X++ class (or classes, in this case) which consists of the Control and BuildControl classes. The BuildControl class is where we define our controls public properties that once dropped on a form can be set by the X++ developer, and maintained during runtime. The source for our class is shown below.

/// <summary>
/// Build Control for 3D STL Viewer
/// </summary>
[FormDesignControlAttribute("XalSTL")]
class XalSTLControlBuild extends FormBuildControl
{
    str url = "";
    int innerHeight = 540;
    int innerWidth = 1024;
    int objectColor = 925765; //dark blue
    int objectShininess = 200;

}

[FormDesignPropertyAttribute("URL","XalSTL")]
public str parmURL(str _url=url)
{
    if (prmIsDefault(_url))
    {
        url = _url;
    }
    return url;
}

[FormDesignPropertyAttribute("InnerHeight","XalSTL")]
public int parmInnerHeight(int _innerHeight=innerHeight)
{
    if (prmIsDefault(_innerHeight))
    {
        innerHeight = _innerHeight;
    }
    return innerHeight;
}

[FormDesignPropertyAttribute("InnerWidth","XalSTL")]
public int parmInnerWidth(int _innerWidth=innerWidth)
{
    if (prmIsDefault(_innerWidth))
    {
        innerWidth = _innerWidth;
    }
    return innerWidth;
}

[FormDesignPropertyAttribute("ObjectColor","XalSTL")]
public int parmObjectColor(int _objectColor=objectColor)
{
    if (prmIsDefault(_objectColor))
    {
        objectColor = _objectColor;
    }
    return objectColor;
}

[FormDesignPropertyAttribute("ObjectShininess","XalSTL")]
public int parmObjectShininess(int _objectShininess=objectShininess)
{
    if (prmIsDefault(_objectShininess))
    {
        objectShininess = _objectShininess;
    }
    return objectShininess;
}

 

Next up is our main control class, with source below. Not much happening here, just basic framework stuff.

/// <summary>
/// Defines a 3D STL Viewer Control
/// </summary>
[FormControlAttribute('XalSTL','',classstr(XalSTLControlBuild))]
class XalSTLControl extends FormTemplateControl
{
    FormProperty url;
    FormProperty innerHeight;
    FormProperty innerWidth;
    FormProperty objectColor;
    FormProperty objectShininess;

}

protected void new(FormBuildControl _build, FormRun _formRun)
{
    super(_build, _formRun);
 
    this.setTemplateId('XalSTL');
    this.setResourceBundleName('/resources/html/XalSTL');

    url = properties.addProperty(methodStr(XalSTLControl, parmURL), Types::String);
    innerHeight = properties.addProperty(methodStr(XalSTLControl, parmInnerHeight), Types::Integer);
    innerWidth = properties.addProperty(methodStr(XalSTLControl, parmInnerWidth), Types::Integer);
    objectColor = properties.addProperty(methodStr(XalSTLControl, parmObjectColor), Types::Integer);
    objectShininess = properties.addProperty(methodStr(XalSTLControl, parmObjectShininess), Types::Integer);
}

[FormPropertyAttribute(FormPropertyKind::Value, "URL")]
public str parmURL(str _value = url.parmValue())
{
    if (!prmIsDefault(_value))
    {
        url.setValueOrBinding(_value);
    }
    return url.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "InnerHeight")]
public int parmInnerHeight(int _value = innerHeight.parmValue())
{
    if (!prmIsDefault(_value))
    {
        innerHeight.setValueOrBinding(_value);
    }
    return innerHeight.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "InnerWidth")]
public int parmInnerWidth(int _value = innerWidth.parmValue())
{
    if (!prmIsDefault(_value))
    {
        innerWidth.setValueOrBinding(_value);
    }
    return innerWidth.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "ObjectColor")]
public int parmObjectColor(int _value = objectColor.parmValue())
{
    if (!prmIsDefault(_value))
    {
        objectColor.setValueOrBinding(_value);
    }
    return objectColor.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "ObjectShininess")]
public int parmObjectShininess(int _value = objectShininess.parmValue())
{
    if (!prmIsDefault(_value))
    {
        objectShininess.setValueOrBinding(_value);
    }
    return objectShininess.parmValue();
}

public void applyBuild()
{
    super();
 
    XalSTLControlBuild build = this.build();
 
    if (build)
    {
        this.parmURL(build.parmURL());
        this.parmInnerHeight(build.parmInnerHeight());
        this.parmInnerWidth(build.parmInnerWidth());
    }
}

 

We also add the control HTML which contains little more than a DIV which we will use as a canvas for our 3D viewer. I reference four additional files containing a modified version of the THREEJS library, which I’ll share upon request.

<meta name="viewport" content="width=1024, user-scalable=no, initial-scale=0.5, minimum-scale=0.2, maximum-scale=0.5">

src="/resources/scripts/three.js">
src="/resources/scripts/STLLoader.js">
src="/resources/scripts/Detector.js">
src="/resources/scripts/OrbitControls.js">
id="XalSTL" style="max-height:400px;" data-dyn-bind="visible: $data.Visible">
/>
/>
src="/resources/scripts/XalSTL.js">

 

Finally, our control JavaScript contains the nuts and bolts that ties all this down into our control and makes this all work, fast and efficient, in D365. You’ll notice that our control has a URL parameter, and this allows us to store our (large) 3D models in Azure Blob Storage or via the dedicated storage account available within D365, via X++ code.

(function () {
    'use strict';
    $dyn.controls.XalSTL = function (data, element) {
        $dyn.ui.Control.apply(this, arguments);
        $dyn.ui.applyDefaults(this, data, $dyn.ui.defaults.XalSTL);
    };
 
    $dyn.controls.XalSTL.prototype = $dyn.ui.extendPrototype($dyn.ui.Control.prototype, {
        init: function (data, element) {
            var self = this;

            var _url = "";
            var _innerHeight = 540;
            var _innerWidth = 1024;
            var _objectColor = 0x0e2045;
            var _objectShininess = 200;

            $dyn.ui.Control.prototype.init.apply(this, arguments);
 
            if (!Detector.webgl) Detector.addGetWebGLMessage();
            var camera, scene, renderer;
            scene = new THREE.Scene();
            scene.add(new THREE.AmbientLight(0x999999));
            camera = new THREE.PerspectiveCamera(35, _innerWidth / _innerHeight, 1, 500);
            camera.up.set(0, 0, 1);
            camera.position.set(0, -9, 6);
            camera.add(new THREE.PointLight(0xffffff, 0.8));
            scene.add(camera);
            var grid = new THREE.GridHelper(25, 50, 0xffffff, 0x555555);
            grid.rotateOnAxis(new THREE.Vector3(1, 0, 0), 90 * (Math.PI / 180));
            scene.add(grid);
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setClearColor(0x999999);
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(_innerWidth, _innerHeight);
            $(".XalSTL").context.activeElement.appendChild(renderer.domElement)

            $dyn.observe(data.URL, function (url) {
                if (url.toString().length > 0) {
                    _url = url;
                    RefreshModel();
                }
            });

            $dyn.observe(data.InnerHeight, function (innerHeight) {
                _innerHeight = innerHeight;
                RefreshModel();
            });

            $dyn.observe(data.InnerWidth, function (innerWidth) {
                _innerWidth = innerWidth;
                RefreshModel();
            });

            $dyn.observe(data.ObjectColor, function (objectColor) {
                _objectColor = objectColor;
                RefreshModel();
            });

            $dyn.observe(data.ObjectShininess, function (objectShininess) {
                _objectShininess = objectShininess;
                RefreshModel();
            });

            function RefreshModel()
            {
                if (_url.toString().length > 0) {
                    var loader = new THREE.STLLoader();
                    var material = new THREE.MeshPhongMaterial({ color: _objectColor, specular: 0x111111, _objectShininess: 200 });
                    var controls = new THREE.OrbitControls(camera, renderer.domElement);
                    loader.load(_url, function (geometry) {
                        var mesh = new THREE.Mesh(geometry, material);
                        mesh.position.set(0, 0, 0);
                        mesh.rotation.set(0, 0, 0);
                        mesh.scale.set(.02, .02, .02);
                        mesh.castShadow = true;
                        mesh.receiveShadow = true;
                        scene.add(mesh);
                        render();
                        controls.addEventListener('change', render);
                        controls.target.set(0, 1.2, 2);
                        controls.update();
                        window.addEventListener('resize', onWindowResize, false);
                    });
                }
            }

            function onWindowResize() {
                camera.aspect = _innerWidth / _innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(_innerWidth, _innerHeight);
                render();
            }

            function render() {
                renderer.render(scene, camera);
            }
        }
    });
})();

 

We can construct a basic demo dialog form as shown below, hit the Reload button and wait for the magic. Using the mouse, we can zoom in and out, and rotate the object in 3D.

XalSTL

Adding a Fabricate button can trigger an event to kick-start our 3D printing process. To tie this all together we can use the rest of the services in D365 for a proper end to end manufacturing pipeline containing CRM, BOM, Invoicing, Projects and everything else normally involved in manufacturing, without leaving the D365 UI.

For a demo video showing this control in action, click here

Advertisements

GPS, IoT, Blockchain Integration to ERP

I’ve read a number of articles discussing how Blockchain could have a significant impact on Trade & Logistics, especially item tracking. Granted, Blockchain is not a requirement for shipment tracking, but it does deliver a number of benefits through being a shared, secure ledger that, depending on the network, could provide an automatic openness almost immediately. That is a vast improvement over building a custom customer and partner portal to query legacy backend systems.

Of course, there remains the problem of now having to integrate Blockchain into your legacy ERP system, a whole different level of headache. So, in this post I’m going to do a simple POC to simulate how easy, or hard, it would be to build an item tracking service using Ethereum Blockchain, add to that GPS tracking with temperature and humidity monitoring, and get that to your ERP system, in this case Microsoft Dynamics 365. I want to achieve that without modifying the ERP system in any way, by using Microsoft Flow, a PowerApp and Microsoft Common Data Service. The idea is that end users, customers or partners can use the PowerApp to monitor shipments and climate conditions in real-time. Supply-chain visibility every step of the way, basically.

To start, I built a simple IoT monitoring device around the Adafruit Huzzah. I’ll be using WiFi here, making a wild assumption that WiFi is available wherever this device goes. In the real world, GPRS or Loran might be more suitable, but I don’t have that available in my toolkit just yet and besides, this is an experiment only. I’ve added a low-cost GPS, DHT11 temperature and humidity sensor, and an LCD screen to show me what is happening without requiring connecting to my laptop via the serial interface. The basic IoT device is shown below, with GPS and DHT-11 working and transmitting data.

Circuit

The C code for the IoT device is shown below. I do a POST to my Ethereum network of choice with hardcoded addresses, and embed the GPS coordinates and DHT11 state into the data portion of the Ethereum transaction. Addressing and data is entirely up to you; perhaps instead of hardcoding, this can all be read off an SD card.

#include <DHT.h>
#include "TinyGPS++.h"
#include <SoftwareSerial.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include "Adafruit_GFX.h"
#include "Adafruit_SSD1306.h"

#define SSID "WiFiSSID" 
#define PASS "mypassword" 
#define OLED_RESET LED_BUILTIN
#define DHTPIN 12
#define DHTTYPE DHT11

TinyGPSPlus gps;
DHT dht(DHTPIN, DHTTYPE);
Adafruit_SSD1306 display(OLED_RESET);
SoftwareSerial mySerial(13, 15);

const char *gpsStream =
  "$GPRMC,045103.000,A,3014.1984,N,09749.2872,W,0.67,161.46,030913,,,A*7C\r\n"
  "$GPGGA,045104.000,3014.1985,N,09749.2873,W,1,09,1.2,211.6,M,-22.5,M,,0000*62\r\n"
  "$GPRMC,045200.000,A,3014.3820,N,09748.9514,W,36.88,65.02,030913,,,A*77\r\n"
  "$GPGGA,045201.000,3014.3864,N,09748.9411,W,1,10,1.2,200.8,M,-22.5,M,,0000*6C\r\n"
  "$GPRMC,045251.000,A,3014.4275,N,09749.0626,W,0.51,217.94,030913,,,A*7D\r\n"
  "$GPGGA,045252.000,3014.4273,N,09749.0628,W,1,09,1.3,206.9,M,-22.5,M,,0000*6F\r\n";

void setup() {
  Serial.begin(9600);
  dht.begin();
  display.setCursor(0,0);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.display();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.println("Connecting");
  display.println("to");
  display.println("WiFi...");
  display.display();

  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  } 
  display.clearDisplay();
  display.display();
  display.setCursor(0,0);
  display.println("EtherGPS");
  display.println("www.xalentis.com");
  display.display();
  mySerial.begin(38400); // GPS
  delay(5000); // warm up GPS 
  display.clearDisplay();
  display.display();
  display.setCursor(0,0);
  display.println("Scanning...");
  display.display();
}

void loop() 
{
  while (*gpsStream)
    if (gps.encode(*gpsStream++))
      updateInfo();
}

void updateInfo()
{
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  if (gps.location.isValid())
  {
    display.clearDisplay();
    display.display();
    display.setCursor(0,0);
    display.println("Temp:" + String(t));
    display.println("Hum:" + String(h));
    display.println("Lat:" + String(gps.location.lat()));
    display.println("Lon:" + String(gps.location.lng()));
    display.display();

    String data = "0x";
    String deviceSerial = "3132333435"; // 12345 in HEX
    data = data + deviceSerial + "2c"; // comma
    
    String temp = String(t);
    String hum = String(h);
    String lat = String(gps.location.lat());
    String lon = String(gps.location.lng());
    byte buffer[255]={0};
    
    //temp
    temp.getBytes(buffer, 255, 0);
    for (int i=0;i<=temp.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }
    data = data + deviceSerial + "2c"; // comma

    //hum
    hum.getBytes(buffer, 255, 0);
    for (int i=0;i<=hum.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }
    data = data + deviceSerial + "2c"; // comma

    //latitude
    lat.getBytes(buffer, 255, 0);
    for (int i=0;i<=lat.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }
    data = data + deviceSerial + "2c"; // comma

    //longitude
    lon.getBytes(buffer, 255, 0);
    for (int i=0;i<=lon.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }

    // build up our Ethereum transaction
    StaticJsonBuffer<1000> JSONbufferTwo;  
    JsonObject& uploadJSON = JSONbufferTwo.createObject(); 
    uploadJSON["jsonrpc"] = "2.0";
    uploadJSON["method"] = "personal_sendTransaction";      
    JsonArray&  uploadQueryParams = uploadJSON.createNestedArray("params");
    JsonObject& callTxParams = JSONbufferTwo.createObject();
    callTxParams["from"] = "0x27f6f763ae5c52721db57c4423c298a78de1f22a";
    callTxParams["to"] = "0xcaade3aa018d57d808fceb16824c47dfd206484c";
    callTxParams["value"] = "0x6FC23AC00"; //hex value 30 Gwei 
    callTxParams["gas"] = "0x30D40"; //hex value for 200000 -high gas limit for good measure          
    callTxParams["gasPrice"] = "0x6FC23AC00"; //hex value 30 Gwei gasprice 21gwei is typical
    callTxParams["data"] = data; // device,tem,hum,lat,long
    uploadQueryParams.add(callTxParams);
    uploadQueryParams.add("myetherpassword");
    uploadJSON["id"] = 1;
    String uploadString;
    uploadJSON.printTo(uploadString);
    callGeth(uploadString); // send for mining
  }
}

String callGeth(String inputJSON) 
{
  HTTPClient http;
  http.begin("http://13.72.73.21:8545/");
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.POST(inputJSON);
  String JSONResult = http.getString(); // contains Txn
  http.end();
  return JSONResult;
}

At this point the transactions are flowing to the Blockchain network and that is great, but we need to be able to monitor the Blockchain for transactions we are interested in, so we can pull that off the Blockchain and into an ERP system, right?

The easiest way to do that is to use Xalentis Fusion. Sign up for a trial account at www.xalentis.com or grab it via Microsoft AppSource. Once signed-up, and logged-in, you’ll end up at the main dashboard as shown below.

xalgps1

Follow the Getting Started tutorial which takes about 10 minutes to create a pair of accounts and top them up with credit as required. The Ethereum network being used is a canned version of Microsoft’s Project Bletchley, so it’s not on the main or test Ethereum networks and can be used without spending any real Ether. You can deploy your own network and use that within the Fusion platform as well, by creating transaction filters pointing to your own deployed RPC node. Make sure your RPC node is visibly outside your firewall, obviously.

The following image shows us having created a transaction filter to monitor the default RPC node at http://xaleth4kq.eastus.cloudapp.azure.com, for any transactions made from the address 0x27f6f763ae5c52721db57c4423c298a78de1f22a. Filters can be created to match any transaction, from any address, or even containing a specific string value in the data portion. This is useful when the address(es) constantly change, while a specific identifier is passed within the data portion, perhaps a Device ID, Company ID or Serial Number of sorts – anything static.

xalgps2

Filters execute rules containing a simple compiled script, and this is where actions are performed on matching transactions. The script below has been added as a rule for our filter.

xalgps3

The rule simply extracts whatever is in the transaction data field, parses that and constructs a JSON packet. This packet will be passed to a Microsoft Flow we will be creating.

We’ll need a place to store our data. Using Flow, we could push directly into Dynamics 365, but since we don’t want to directly modify our ERP by adding a new table, I’ve chosen to use Microsoft Common Data Service as a temporary store instead. The image below shows the new Entity we’ve created with fields for Device, Temperature, Humidity, Longitude and Latitude.

GPS_CDS

Using Microsoft Flow, we’ll first use the Request action to accept an incoming POST request (from our Rule). Next, we’ll take the body of the POST request, parse it, and store the fields into our new CDS Entity. The Flow design is shown below.

GPS_Flow

Use the generated URL from the Flow to update the Rule – the final line calling Flow requires that URL.

Run the Flow, and then power up the IoT device to start submitting GPS and climate information into the Blockchain network. As transactions are mined into new blocks, Fusion will detect the transaction matching our Filter, and execute the associated Rule. The Rule in turn will parse the transaction data field, parse the content, construct it as JSON, and call our Flow with that body content. When the Flow executes, the JSON will be parsed and the data elements inserted as a new record into the CDS, as shown below.

GPS_DataCDS

We can use the data now stored in the CDS to create a PowerApp that displays that information on a Google Map. The PowerApp shown is fairly basic, but with enough time, patience and data this can be turned into something much more interactive, and it is real-time, a vast improvement over building a customer tracking portal from scratch, getting updates only when items are scanned with a barcode or RFID reader.

GPS_Map

Apart from our Rule script, we’ve used virtually no coding, and we’ve not modified our production ERP system in any way. As a bonus, we also have a mobile app that customers and partners can use!

RFID + IoT + Ethereum Blockchain

This is a very quick post, mostly code-only, showing how to read RFID tags on say an assembly line, process those using a WiFi IoT device (Adafruit Huzzah), extract the product serial number from the RFID tag and send that as part of a transaction to an Ethereum Blockchain.

I am using a MiFare RFID card reader, but you would want to use a long-range reader that supports low-cost sticker tags. POSTing the transaction to the blockchain takes a second or two, so don’t expect to scan 100 products/second flying past the reader and manage to submit those as transactions.

URL’s and codes are hard-baked, so consider how you want to post the transaction. I use static Ethereum addresses and stick the product ID in the data portion, you might want to read that from the RFID tags as well.

Once your product ID is in the blockchain, you might want to move that to your ERP system, or even a cool PowerApp or something. Unless you want to code X++ and mess around with your production systems, I suggest using Xalentis Fusion instead, to enable code-free integration between Ethereum and Microsoft Dynamics 365 for Finance and Operations. It also supports SMS, Email messaging, Service Bus messaging, Flow, PowerApps and Common Data Service.

Enjoy.

#include <SPI.h>
#include <MFRC522.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
 
#define RST_PIN         15
#define SS_PIN          2
#define SSID            "mywifiSSID" 
#define PASS            "password" 
 
MFRC522 mfrc522(SS_PIN, RST_PIN);
 
void setup() {
  Serial1.begin(115200);
  while(!Serial1){}
  Serial.begin(9600);
  SPI.begin();
  mfrc522.PCD_Init();
  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
}
 
void loop()
{
  // Using MiFare card reader here, for production use sticker tags and high-speed reader instead
  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 RFID tag is presented 
  if (!mfrc522.PICC_IsNewCardPresent()) return;
  if (!mfrc522.PICC_ReadCardSerial()) 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) {
    return;
  }
  else
  {
    for (int i = 0; i < 18; i++)
    {
      readbuffer1[i] = 0x00;
      readbuffer2[i] = 0x00;
    }
    // read product ID from RFID tag
    status = mfrc522.MIFARE_Read(1, readbuffer1, &sizeread);
    if (status != MFRC522::STATUS_OK)
    {
      return;
    }
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
  }
 
  // convert product ID from RFID tag to hex string
  String data = "0x";
  for (int j=0; j<18;j++)
  {
    if (readbuffer1[j]=='\0') break;
    data = data + String(readbuffer1[j], HEX);
  }

  // build up our Ethereum transaction
  StaticJsonBuffer<1000> JSONbufferTwo;  
  JsonObject& uploadJSON = JSONbufferTwo.createObject(); 
  uploadJSON["jsonrpc"] = "2.0";
  uploadJSON["method"] = "personal_sendTransaction";      
  JsonArray&  uploadQueryParams = uploadJSON.createNestedArray("params");
  JsonObject& callTxParams = JSONbufferTwo.createObject();
  callTxParams["from"] = "0x27f6f763ae5c52721db57c4423c298a78de1f22a";
  callTxParams["to"] = "0xcaade3aa018d57d808fceb16824c47dfd206484c";
  callTxParams["value"] = "0x6FC23AC00"; //hex value 30 Gwei 
  callTxParams["gas"] = "0x30D40"; //hex value for 200000 -high gas limit for good measure          
  callTxParams["gasPrice"] = "0x6FC23AC00"; //hex value 30 Gwei gasprice 21gwei is typical
  callTxParams["data"] = data;
  uploadQueryParams.add(callTxParams);
  uploadQueryParams.add("bigsecretaccountpassword");
  uploadJSON["id"] = 1;
  String uploadString;
  uploadJSON.printTo(uploadString);
  callGeth(uploadString); // send for mining
}

String callGeth(String inputJSON) // thanks to https://github.com/gusgorman402
{
  HTTPClient http;
  http.begin("http://your RPC address here:8545/");
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.POST(inputJSON);
  String JSONResult = http.getString(); // contains Txn
  http.end();
  return JSONResult;
}

 

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.

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

 

 

 

 

Routing WiFi-enabled IoT Devices

While testing various projects using the ESP8266 WiFi chip I found myself wondering about the practicalities of manufacturing and distributing pre-programmed IoT devices to business and consumers alike. Sitting at a laptop connected to your IoT device of choice is relatively painless. You can sketch something up in C that reads sensors and does all kinds of neat stuff, sending that data into the cloud for consumption and analysis, but that all works fine while you’ve got your home or lab WiFi router SSID and Password coded into your sketch.

Once your project goes into manufacturing, that SSID and Password no longer applies, and it has to be configured somehow by the end-user to of course match whatever AP and security they have on-site. Chances are if your consumer IoT device was bulk-manufactured and pre-programmed in say China, there is a less than zero chance it will work out of the box. To complicate matters even more, customers tend to regularly change AP configurations, rendering your IoT device useless unless reconfigured or reprogrammed.

You of course have several options available to bypass this problem including:

  • Require the user to have an open AP for your device to connect to
  • Require the user to have a secure AP with a specific SSID and password available to match what is hardcoded in your device
  • Give the user the ability to reconfigure the device using their own machine and a USB cable, or maybe via Bluetooth
  • Give them a utility for configuration onto SD card, which is then installed in the device if supported

All of the above solutions creates a new set of problems:

  • Security needs to be dumbed down or
  • Painful reconfiguration is required by a specialist during installation or
  • Your IoT manufacturing cost now needs to include SD card and/or Bluetooth, when it will end up using WiFi and already include that in the manufacturing cost
  • Add an LCD screen with configuration buttons and a user manual in multiple languages – as simple as programming a VCR was back in the 80’s

An alternative approach is offered with the IoTLink platform (www.iotlink.net) which I shamelessly promote as it is my platform. With IoTLink, you can utilize a temporary AP (iPhone, Android, pocket WiFi router) on-site, either by a technician or through simple instructions to the end user. There are numerous apps available for both iPhone and Android which allows you to use them as a temporary AP where you can define the SSID and Password.

This “required” SSID and Password can be documented with your shipped device, allowing the user to find a temporary Internet connection, via temporary AP (phone), which then connects to IoTLink where both the device vendor as well as the end user are given the ability to whitelist and configure the IoT device to then link to whatever secure AP is available at the user site, whether at home or in a business scenario. It also allows reconfiguration and can collect a reasonable amount of diagnostic information for both end user and the device vendor.

There is a lot more information available at the IoTLink site, with a number of examples which shows how IoTLink can be incorporated into your project. As a quick example in this post, I will show how to program an ESP8266 hooked up to an Arduino Mega to:

  • Find the first available open AP and connect to it
  • Use it to look up proper secure customer AP information from the IoTLink directory
  • Disconnect from temporary AP and establish a secure connection to the customer AP. This example does not use SSL but IoTLink supports both SSL and open connections (for extremely limited devices or testing scenarios).

 

#include <ESP8266.h>

String SSID;
bool routed = false;

ESP8266 wifi(Serial2);

void setup(void)
{
  Serial2.begin(115200);
  Serial.begin(9600);  
     
  wifi.setOprToStationSoftAP();
}

void loop(void)
{
  if (!routed)
  {
    // find an open AP
    String apList = wifi.getAPList();

    bool haveOpenAP = false;
    if (apList.indexOf("+CWLAP:(0,\"") >=0) // find first open AP
    {
      haveOpenAP = true;
      apList = apList.substring(apList.indexOf("+CWLAP:(0,\"") + 11, apList.length() - (apList.indexOf("+CWLAP:(0,\"") + 11));
      apList = apList.substring(0, apList.indexOf("\""));
      if (wifi.joinAP(apList))
      {
        wifi.disableMUX();
        route();
      }
    }
  }
  
  // your code here

}

// IoTLink
void route(void)
{
  uint8_t buffer[1024] = {0};
  wifi.createTCP("www.iotlink.net", 80);
  char *hello = "GET /route?deviceid=DEV12345 HTTP/1.1\r\nHost: www.iotlink.net\r\nConnection: close\r\n\r\n";
  wifi.send((const uint8_t*)hello, strlen(hello));

  uint32_t len = wifi.recv(buffer, sizeof(buffer), 10000);
  if (len > 0)
  {
    char ssid_new[20]={0};
    char pass_new[20]={0};
    char *content = strstr((char *)buffer, "\r\n\r\n");
    if (content != NULL) content += 4;
    String sr(content);
    String ssidx = getValue(sr,',',0);
    String passx = getValue(sr,',',1);

    ssidx.toCharArray(ssid_new,20);
    passx.toCharArray(pass_new,20);
    wifi.leaveAP();
    delay(1000);
    wifi.joinAP(ssid_new, pass_new);
    routed = true;
  }
  wifi.releaseTCP();
}

// 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]) : "";
}

IoT Dashboard in AX using Control Extensibility

I’ve been looking into new the control extensibility API of the latest version of Dynamics AX and it has a lot of potential. Unfortunately, my first thought when reading about it was that it could lead to a way to develop “Custom Controls” pluggable from the ToolBox, similar to other popular commercial toolsets like grids. Sadly, that is not the case (it seems) which is unfortunate. It can potentially create a substantial commercial marketplace for extensions, if possible. Perhaps I missed the point, or maybe Microsoft has plans for it in the future. I sincerely hope so.

Until then, we can still use the control extensibility API to develop small HTML based elements with custom properties, and the first stop to make would be at GitHub where Sharrief Shabazz from Microsoft has uploaded some good examples to get you going.

So starting with the BasicValueControl example, I’m going to play around and see what I can do. First, let’s create our three items required to make this work. We’ll create an HTML file and stick an IMG tag into then, then bind the SRC attribute back to our X++ code.

id="BasicControl"> data-dyn-bind="attr: {src: $control.Url}"/>

 

The X++ code that drives this binding is shown below. We’ve added a Url property which we will access from our Form.

[FormControlAttribute("BasicControl","resources/html/BasicControl", classstr(FormBuildControl))]
class IoTControl extends FormTemplateControl
{
    #define.ValuePropertyName("Url")

    FormProperty valueProperty;

    public void new(FormBuildControl _build, FormRun _formRun)
    {
        super(_build,_formRun);

        this.setTemplateId("BasicControl");
        this.setResourceBundleName("resources/html/BasicControl");

        valueProperty = this.addProperty(methodStr(IoTControl, parmValue), Types::String);

    }

    [FormPropertyAttribute(FormPropertyKind::Value, #ValuePropertyName, true)]
    public str parmValue(str _value = valueProperty.parmValue())
    {
        if(!prmisdefault(_value))
        {
            valueProperty.setValueOrBinding(_value);
        }
        return valueProperty.parmValue();
    }
}

 

So let’s create a Form based on the Custom pattern and see if we can access an offsite image located somewhere on the web. I’ll stick the image URL into the HTML controls’ property and run the form.

[Form]
public class IoTForm extends FormRun
{
    IoTControl iotControl;
    /// <summary>
    ///
    /// </summary>
    public void init()
    {
        super();

        iotControl = this.design().addControlEx(classStr(IoTControl),"IoTControl1");
        iotControl.parmValue("http://www.xxxxxxxx.com/xxxxx.png");
    }
}

 

Okay so that works well and raises a number of interesting use cases. First, instead of having to store a repository of images for your Fleet Management system inside the AX database for say, parts or stock, we can instead point the image to another site where we might have already built up a substantial library of images. Excellent.

ExControlImage

Now let’s take this a bit further. Say we have an IoT hub of sorts, receiving a number of sensor updates, and we want to create a dashboard showing these values in Dynamics AX. Let’s assume that hub is not running on Azure nor Power BI, but on another popular platform called ThingSpeak.

ThingSpeak provides a REST interface for both pushing IoT data into, and then retrieving back using the same API. So we can access the latest field values from a channel in ThingSpeak with a simple call, and with a bit of luck display this back in AX. Let’s have a look at our updated HTML control below.

id="BasicControl"> type="text" data-dyn-bind="attr: {value: $control.Url}"/>

 

We’ve modified our class a little to make the WebRequest call to ThingSpeak, given our channel, API Key and field for temperature (field 1).

[Form]
public class IoTForm extends FormRun
{
    IoTControl iotControl;

    public void init()
    {
        super();

        iotControl = this.design().addControlEx(classStr(IoTControl),"IoTControl1");

        System.Net.HttpWebRequest myRequest;
        System.Net.HttpWebResponse myResponse;
        System.IO.StreamReader reader;
        str iotResult;

        myRequest = System.Net.WebRequest::Create("http://api.thingspeak.com/channels/98558/fields/1/last?key=xxxxxxxxxx”);
        myResponse = myRequest.GetResponse();
        System.IO.Stream stream = myResponse.GetResponseStream();
        reader = new System.IO.StreamReader(stream);
        iotResult = reader.ReadToEnd();

        iotControl.parmValue(iotResult);
    }
}

 

So below we see this works, we receive back “23” into the control which is correct.

IOTControl2

A bit ugly so we can style the HTML slightly as shown below:

id="BasicControl">
type="text" data-dyn-bind="attr: {value: $control.Url}" style="padding:3px; border-color:#cccccc; text-align:center; color:#000000; font-weight:bold; font-size:30px; border-style:solid; border-width:5px; border-radius:17px; box-shadow:0px 0px 4px 0px rgba(42,42,42,.75);"/>

 

That looks a bit better.

IOT2Styled

To improve on this, let’s add two controls, one displaying temperature and the other humidity, and then create a reusable helper class for making the API calls. We’ll update the Form as shown below.

[Form]
public class IoTForm extends FormRun
{
    IoTControl iotControl1;
    IoTControl iotControl2;

    public void init()
    {
        super();

        iotControl1 = this.design().addControlEx(classStr(IoTControl),"IoTControl1");
        iotControl2 = this.design().addControlEx(classStr(IoTControl),"IoTControl2");
        
        str temp = IoTAPI::GetResult("http://api.thingspeak.com/channels/98558/fields/1/last?key=xxxxxx”);
        iotControl1.parmValue(temp);

        str hum = IoTAPI::GetResult("http://api.thingspeak.com/channels/98558/fields/2/last?key=xxxxxxx”);
        iotControl2.parmValue(hum);
    }

}

 

Our helper class accepts the URL and returns the response, as simple as it gets. You might want to add an exception handler that returns an error message instead of the value; always a good idea.

class IoTAPI
{
    public static str GetResult(str theUrl)
    {
        System.Net.HttpWebRequest myRequest;
        System.Net.HttpWebResponse myResponse;
        System.IO.StreamReader reader;
        str iotResult;

        myRequest = System.Net.WebRequest::Create(theUrl);
        myResponse = myRequest.GetResponse();
        System.IO.Stream stream = myResponse.GetResponseStream();
        reader = new System.IO.StreamReader(stream);
        iotResult = reader.ReadToEnd();
        return iotResult;
    }
}

 

So here we have the two controls shown on the form, opening up a world of new possibilities into offsite integration, data retrieval, API calls to protect IP by keeping the logic offsite, etc. etc.

IoTDashboard

Hope this post was worth your time!