Introduction to the IoT Edge SDK V1, part 6: Modbus for Wise modules

Update: Recently, Microsoft introduced the new V2 version of their IoT Edge. This blog is referencing the former but still supported V1.

I do a lot of IoT research using the Advantech Wise 4012E IO module. It’s biggest advantages are that it runs on 5 Volts, using a MicroUSB connection (instead of a bulky 12V or 24V adapter) and it comes with knobs, switches and LEDs to simulate real sensors. So it’s a very compact but yet complete IoT device.

Until a few days ago I made use of the Rest protocol to contact that module but this has some disadvantages.

First, Rest is a great protocol in the IT world but it is not used that much in the OT world. Luckily, the IO module also supports the Modbus protocol. So I tried to switch to that protocol.

Second, Rest is very slow compared to other protocols like Modbus. Using Rest, I’m lucky when I can pull 2 or 3 requests a second out of the module.

In the previous blogs, we have seen multiple modules, both provided by Microsoft or created on the fly. And Microsoft provides a genuine Modbus module for the IoT Edge SDK. There is only one drawback, it’s on Github but it’s not available as NuGet package. You have to make/build it yourself!

And for some unknown reason, I did not get it working the way I liked it. I encountered too many exceptions.

Update: There is also a Version 2 of this module. In this blog, I refer to V1.

So, in the end, I just ignored the module and build my own Modbus module.

This blog is the fourth part of a series:

  • Part one, how to use modules, gateway configuration and the broker
  • Part two, connecting to the IoT Hub
  • Part three, message to device
  • Part four, IoT Hub Routing
  • Part five, Watchdog
  • Part six, Modbus for Advantech Wise modules

Which Modbus library to use?

There are multiple NuGet packages to choose from. I checked them all out but finally, I have chosen the one with the highest amount of download: NModbus. And this library is still updated by its creators.

Calling the Wise module

There is documentation for the Wise Module and how to refer to it using Modbus.

So I checked out the NModbus code to call the Wise IO module:


...
public ModbusProvider(string ipAddress)
{
  _client = new TcpClient(ipAddress, 502);

  var factory = new ModbusFactory();

  _master = factory.CreateMaster(_client);
}

public void ReadSensors()
{
  // Knobs
  var readHoldingRegisters = _master.ReadHoldingRegisters(0, 0, 3);
  KnobOne = readHoldingRegisters[0];
  KnobTwo = readHoldingRegisters[1];
  KnobAvg = readHoldingRegisters[2];
  // Switches
  ushort startAddress = 0;
  ushort numInputs = 2;
  var switches = _master.ReadInputs(0, startAddress, numInputs);
  SwitchOne = switches[0];
  SwitchTwo = switches[1];
}

...

_master.WriteSingleCoil(0, 16, value); // 17 linker relay

...
_master.WriteSingleCoil(0, 17, value);  // 18 rechter relay

...

It took some time to find the correct registers and coils of the Modbus interface but finally I got it working.

The full library is now available on GitHub. It comes with an example client to test the library.

Using it in an IoT Edge Module

So I used this open source library to access the Wise 4012e IO module using the IoT Edge SDK:

namespace Edge.ClassLibrary
{
    using Microsoft.Azure.IoT.Gateway;
    using Newtonsoft.Json;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using Wise.E4012.Modbus;
 
    public class IoTDeveloperModbusModule : IGatewayModule, IGatewayModuleStart
    {
        private Broker _broker;
        private IoTConfigModbus _ioTConfigModbus;
        private int _interval;
 
        public void Create(Broker broker, byte[] configuration)
        {
            _broker = broker;
 
            var config = Encoding.UTF8.GetString(configuration);
            _ioTConfigModbus = JsonConvert.DeserializeObject<IoTConfigModbus>(config);
 
            _interval = _ioTConfigModbus.interval;
        }
 
        public void Start()
        {
            var oThread = new Thread(new ThreadStart(ThreadBody));
            // Start the thread for reading data and generate message
            oThread.Start();
            Console.WriteLine("SensorModule started");
        }
 
        public void ThreadBody()
        {
            while (true)
            {
                foreach (var device in _ioTConfigModbus.configDevices)
                {
                    try
                    {
                        var ipAddress = device.ipAddress;
 
                        using (var modbusProvider = new ModbusProvider(ipAddress))
                        {
                            modbusProvider.ReadSensors();
 
                            var properties = new Dictionary<string, string>
                            {
                              { "source", "sensor" },
                              { "macAddress", device.macAddress }
                            };
 
                            var telemetryMessage = new IoTTelemetryMessage
                            {
                                humidity = modbusProvider.KnobOne / 100,
                                temperature = modbusProvider.KnobTwo / 100,
                                led = modbusProvider.SwitchOne,
                                fan = modbusProvider.SwitchTwo,
                            };
 
                            var jsonMessage = JsonConvert.SerializeObject(telemetryMessage);
 
                            var messageToPublish = new Message(jsonMessage, properties);
 
                            // Publish the Sensor data
                            _broker.Publish(messageToPublish);
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Exception {ex.Message}");
                    }
                }
 
                // Publish a message every X seconds.
                Thread.Sleep(_interval);
            }
        }
 
        public void Destroy()
        {
        }
 
        public void Receive(Message receivedMessage)
        {
            if (receivedMessage.Properties["source"] == "mapping"
                    && receivedMessage.Properties.ContainsKey("macAddress"))
            {
                // we filter for commands
                var content = Encoding.UTF8.GetString(receivedMessage.Content, 0, receivedMessage.Content.Length);
 
                var device = _ioTConfigModbus.configDevices.FirstOrDefault(x => x.macAddress == receivedMessage.Properties["macAddress"]);
 
                if (device != null)
                {
                    // eg. led=0 or fan=1
                    Console.WriteLine($"SensorModule: device {device.ipAddress} received command {content}");
 
                    var ipAddress = device.ipAddress;
 
                    using (var modbusProvider = new ModbusProvider(ipAddress))
                    {
                        if (content.ToLower().StartsWith("led"))
                        {
                            bool enabled = content.ToLower().Split('=')[1] == "1";
 
                            modbusProvider.RelayOne = enabled;
                        }
 
                        if (content.ToLower().StartsWith("fan"))
                        {
                            bool enabled = content.ToLower().Split('=')[1] == "1";
 
                            modbusProvider.RelayTwo = enabled;
                        }
                    }
                }
            }
        }
    }
}

This class needs the following configuration:

{
  "name": "IoTDeveloperModule",
  "loader": {
    "name": "dotnet",
    "entrypoint": {
      "assembly.name": "Edge.ClassLibrary",
      "entry.type": "Edge.ClassLibrary.IoTDeveloperModbusModule"
    }
  },
  "args": {
    "configDevices": [
      {
        "ipAddress": "192.168.1.xyz",
        "macAddress": "01:01:01:01:01:01"
      }
    ],
    "interval": 5000
  }
}

And to parse the configuration we need the following classes:

public class IoTConfigModbus
{
    public ModbusConfigDevice[] configDevices { get; set; }
    public int interval { get; set; }
 
    public class ModbusConfigDevice
    {
        public string ipAddress { get; set; }
        public string macAddress { get; set; }
    }
}

Using this code I can now read messages 15 times a second or more. Try to do this with Rest:

Here I show the number of calls executed each second. How many do you count?

Conclusion

Using this simple module, we now have full control over the Wise 2012E IO module using Modbus. And you can easely rewrite this to virtually every Modbus device.

Yes, Modbus is not that hard and using the NModbus NuGet package and my open source library from Github makes it plain easy.

So please dive into Modbus, it’s easy, it’s fast.

I hope that Microsoft will provide a simple NuGet package for all the (extra) modules.

Advertenties