Writing commands to IoT Edge Modbus Modules

Microsoft provides several out-of-the-box modules for their Azure IoT Edge platform. If we do a quick search at the Public Docker repository, we see modules like

  • microsoft/iot-edge-opc-publisher
  • microsoft/iot-edge-opc-proxy
  • microsoft/azureiotedge-modbus-tcp
  • etc,

I already have described in a previous blog, how to consume and read data from that Modbus module. After checking out the documentation and some testing, I found out how to write commands back to the device too.

Let’s check out how we can use this in a Custom C# module. After that, we use it in an Azure Functions Module. So let’s do a deeper dive into Azure Functions on the IoT Edge as well.

I assume you already have a Modbus module running on your edge…

Custom C# Module

Writing a command is nothing more than routing a message back to Modbus module in a specific payload format and with an extra property telling the Modbus module we want to write to a Modbus device.

According to the documentation, it looks like this:

{
 "HwId":"PowerMeter-0a:01:01:01:01:01",
 "UId":"1",
 "Address":"40001",
 "Value":"15"
}

And the property looks like this:

"command-type": "ModbusWrite"

To check out if I could let it work, I wrote a C# module. It just listens to Desired Properties and when these are received, a command is sent. So we only need to add a method to listen to desired properties:

static string HwId {get; set;}
static string UId {get; set;}
static string Address {get; set;}
static string Value {get; set;}

static async Task onDesiredPropertiesUpdate(TwinCollection desiredProperties, object userContext)
{
  if (desiredProperties.Count == 0)
  {
    return;
  }

  try
  {
    Console.WriteLine("Desired property change:");
    Console.WriteLine(JsonConvert.SerializeObject(desiredProperties));

    var deviceClient = userContext as DeviceClient;

    if (deviceClient == null)
    {
      throw new InvalidOperationException("UserContext doesn't contain expected values");
    }

    var reportedProperties = new TwinCollection();

    if (desiredProperties["hwId"] != null)
    {
      HwId = desiredProperties["hwId"];
      reportedProperties["hwId"] = HwId;
    }

    if (desiredProperties["uId"] != null)
    {
      UId = desiredProperties["uId"];
      reportedProperties["uId"] = UId;
    }

    if (desiredProperties["address"] != null)
    {
      Address = desiredProperties["address"];
      reportedProperties["address"] = Address;
    }

    if (desiredProperties["value"] != null)
    {
      Value = desiredProperties["value"];
      reportedProperties["value"] = Value;
    }

    Console.WriteLine("Info: Construct message");

    var modbusMessageBody = new ModbusMessage
    {
      HwId = HwId,
      UId = UId,
      Address = Address,
      Value = Value,
    };

    var jsonMessage = JsonConvert.SerializeObject(modbusMessageBody);
    var bytes = System.Text.Encoding.UTF8.GetBytes(jsonMessage);
    Console.WriteLine("Info: Byte array SwitchTwo");
    var pipeMessage = new Message(bytes);
    pipeMessage.Properties.Add("command-type" , "ModbusWrite");

    await deviceClient.SendEventAsync("output1", pipeMessage);

    Console.WriteLine("Info: Piped out message SwitchTwo");

    if (reportedProperties.Count > 0)
    {
    await deviceClient.UpdateReportedPropertiesAsync(reportedProperties).ConfigureAwait(false);
    }
  }
  catch (AggregateException ex)
  {
    foreach (Exception exception in ex.InnerExceptions)
    {
      Console.WriteLine();
      Console.WriteLine("Error when receiving desired property: {0}", exception);
    }
  }
  catch (Exception ex)
  {
    Console.WriteLine();
    Console.WriteLine("Error when receiving desired property: {0}", ex.Message);
  }
}

public class ModbusMessage
{
  public string HwId {get; set;}
  public string UId {get; set;}
  public string Address {get; set;}
  public string Value {get; set;}
}

This method has to be attached to the callback function which listens for desired properties:

// Attach callback for Twin desired properties updates
await ioTHubModuleClient.SetDesiredPropertyUpdateCallbackAsync(onDesiredPropertiesUpdate, ioTHubModuleClient);

// Execute callback method for Twin desired properties updates
var twin = await ioTHubModuleClient.GetTwinAsync();
await onDesiredPropertiesUpdate(twin.Properties.Desired, ioTHubModuleClient);

// Start the client
await ioTHubModuleClient.OpenAsync();
Deploy this module to your Docker repository. You can use mine using “docker pull svelde/msftmodbuscommandtest”.

Now go to the Azure portal and deploy the module to your Edge device. Once it’s running stable at the Edge, submit the following properties (or likewise, depending on your actual Modbus device):

...
"properties": {
  "desired": {
    "hwId": "Wise4012E-142",
    "uId": "1",
    "address": "00017",
    "value": "1",
...

For testing, I use the Advantech Wise 4012E IO-module. This device has two potentiometers (knobs), two switches and two LEDs. The LEDs can be turned on and off using two relays. According to the device documentation, we have to switch on address 17 and 18.

Note: Be sure you add the leading zeros. otherwise, nothing will be switched. Therefore I use “00017”.

Note: It’s not quite sure where uId stands for. I leave it to “1”

Once I have sent these desired properties, I can check the log of the Microsoft Modbus module (I use the alias ‘msftmodbus’). I run:

docker logs -f msftmodbus

I will show the incoming messages. And we will see the arrival of the message:

Modbus Writer - Received command
Received message: 1, Body: [{"HwId":"Wise4012E-142","UId":"1","Address":"17","Value":"1"}]
Write device Wise4012E-142, address: 17, value: 1

Finally, the LED is turned one. This quite simple, isn’t it?

Azure Functions Module

Because we can write to the Modbus, we can build feedback loops. We react to the data read by the Modbus module by evaluating it in the cloud or by evaluating it in a Stream Analytics job and sending a signal to a custom code module. Or we can use a local Azure Function.

Microsoft has already provided documentation on how to construct, build and deploy an Azure Function for the Edge.

It must be clear that an Azure Function is just another Docker image and inside is running an Azure Function environment. It’s executing some code after it gets a message from another module and it can write back messages to the same message bus. You can see that in the function.json file:

{
  "bindings": [
    {
      "type": "edgeHubTrigger",
      "name": "messageReceived",
      "InputName": "input1",
      "direction": "in"
    },
    {
      "type": "edgeHub",
      "name": "output",
      "outputName": "filterOutput",
      "batchSize": 10,
      "direction": "inout"
    }
  ]
}

Here we see the input named ‘input1’ and output ‘filterOutput’. We will need these names for the routing, later on.

Note: Do you notice the new ‘edgeHubTrigger’ trigger type? And do you notice the new ‘edgeHub’ output type?

But first, we look at the code of out Azure Function:

#r "Microsoft.Azure.Devices.Client"
#r "Newtonsoft.Json"

using System.IO;
using System.Linq;
using Microsoft.Azure.Devices.Client;
using Newtonsoft.Json;
public static async Task Run(Message messageReceived, IAsyncCollector<Message> output, TraceWriter log)
{
  try
  {
    byte[] messageBytes = messageReceived.GetBytes();
    var messageString = System.Text.Encoding.UTF8.GetString(messageBytes);

    if (!string.IsNullOrEmpty(messageString))
    {
      log.Info($"Info: Received one non-empty message at {DateTime.Now}");

      var messageBody = JsonConvert.DeserializeObject<MessageBodyItem[]>(messageString);

      log.Info($"Info: deserialized into {messageBody.Length} items");

      var switchOne = messageBody.FirstOrDefault(x=>x.DisplayName == "SwitchOne");

      if (switchOne != null)
      {
        log.Info($"Info: Construct message SwtichOne {switchOne.Value}");

        var modbusMessageBodyOne = new ModbusMessage
        {
          HwId = switchOne.HwId,
          UId ="1",
          Address ="00017",
          Value = switchOne.Value,
        };

        var jsonMessageOne = JsonConvert.SerializeObject(modbusMessageBodyOne);
        var bytesOne = System.Text.Encoding.UTF8.GetBytes(jsonMessageOne);
        log.Info("Info: Byte array SwitchOne");
        var pipeMessageOne = new Message(bytesOne);
        pipeMessageOne.Properties.Add("command-type" , "ModbusWrite");
        await output.AddAsync(pipeMessageOne);
        log.Info("Info: Piped out message SwitchOne");
      }
      else
      {
        log.Info("Info: NO Piped out message SwitchOne");
      }

      var switchTwo = messageBody.FirstOrDefault(x=>x.DisplayName == "SwitchTwo");

      if (switchTwo != null)
      {
        log.Info($"Info: Construct message SwitchTwo {switchTwo.Value}");

        var modbusMessageBody = new ModbusMessage
        {
          HwId = switchTwo.HwId,
          UId ="1",
          Address ="00018",
          Value = switchTwo.Value,
        };

        var jsonMessage = JsonConvert.SerializeObject(modbusMessageBody);
        var bytes = System.Text.Encoding.UTF8.GetBytes(jsonMessage);
        log.Info("Info: Byte array SwitchTwo");
        var pipeMessage = new Message(bytes);
        pipeMessage.Properties.Add("command-type" , "ModbusWrite");
        await output.AddAsync(pipeMessage);
        log.Info("Info: Piped out message SwitchTwo");
      }
      else
      {
        log.Info("Info: NO Piped out message SwitchTwo");
      }
    }
  }
  catch (Exception ex)
  {
    log.Info($"WARN: Azure Modbus Function Exception {ex}");
  }
}

public class MessageBodyItem
{
  public string DisplayName {get; set;}
  public string HwId {get; set;}
  public string Address {get; set;}
  public string Value {get; set;}
  public string SourceTimestamp {get; set;}
}

public class ModbusMessage
{
  public string HwId {get; set;}
  public string UId {get; set;}
  public string Address {get; set;}
  public string Value {get; set;}
}

The function will be triggered by receiving a message from the regular Modbus module. We deserialize it into a C# class. It’s just an array of register and coil values.

What we want to achieve is reacting on the two switches on the Advantech Wise 4012E IO Module. We put on and of the two LED’s on the IO Module according to the two switches.

So we check the array for the two switches and it needed, we put output messages on the ‘output’ array. Yes, we can write multiple messages in an Azure Function at once.

After the code of the messages is checked for typos (no, we have no compiler check at this moment for Azure Functions), build an image and push it to your favorite Docker repository.

Note: I have tried to execute the code in the Azure portal by creating a regular Azure Function and replacing all files (run.csx and function.json). Unfortunately, the native compiler which normally runs is suddenly not available anymore…

Note: I expect we will be able to define Edge functions in the Azure Portal in the near future and we will be able to deploy them from the Azure portal just like Azure Stream Analytics jobs.

Note: You can use my image “docker pull svelde/wise4012e.modbus.feedback.function” or “svelde/wise4012e.modbus.feedback.function:1.0”

Now add the module to the IoT Edge using the tooling in the Azure Portal. Use the alias name ‘feedback’:

And at the following routes:

{
  "routes": {
    "modbusToFilter": 
      "FROM /messages/modules/msftmodbus/outputs/modbusOutput 
        INTO BrokeredEndpoint(\"/modules/feedback/inputs/input1\")",
    "filterToModbus": 
      "FROM /messages/modules/feedback/outputs/filterOutput 
        INTO BrokeredEndpoint(\"/modules/msftmodbus/inputs/input1\")",
    "feedbackToCloud": 
      "FROM /* INTO $upstream"
  }
}

Once the module is deployed, check out the messages in the ‘msftmodbus’ module logging when the first switch is is set to ‘on’ and the second to ‘off’:

Modbus Writer - Received command
Received message: 175, Body: [{"HwId":"Wise4012E-142","UId":"1","Address":"00017","Value":"1"}]
Write device Wise4012E-142, address: 00017, value: 1
Modbus Writer - Received command
Received message: 176, Body: [{"HwId":"Wise4012E-142","UId":"1","Address":"00018","Value":"0"}]
Write device Wise4012E-142, address: 00018, value: 0
00001: 1
00002: 0

Conclusion

We have managed to write commands to the Microsoft Modbus module. We used both a custom C# module and a brand new Azure Functions module.

Advertenties