ModbusTCP Device

Hi All

Hoping somebody can point me in the right direction.

I am trying to set up a ModbusTCP Device (Slave) on a SC20100S Dev Rev C board with a EthClick Module in position 1 on the board.

I have had similar running on a G120 DEV with NetMF4.3 / Visual Basic without a problem and I’m now trying to move to TinyCLR. I apologize if my code formatting is not 100% as I’m not an experienced C# programmer. You could say I don’t have my C legs yet :upside_down_face:

I have distilled the code down to the bare minimum - setting up the network and setting up the Modbus device to allow reading of holding registers only.

The test setup is a laptop (10.1.1.101) running a Modbus test tool (Radzio! Modbus Master Simulator) directly connected to the SC20100 board (10.1.1.1).

I can ping the board no problem.
(Note I “borrowed” a MAC address from my iPad as I thought this may have been a problem initially).

When I get the test tool to request 10 holding registers it initially gets a valid response and correctly reads the values I have set in the holding registers (2). Subsequent request result in a Modbus timeout.

If I “disconnect” the modbus test tool and then reconnect I again get one successful read and then timeouts.

I have captured the network activity using Wireshark - network comms is not my strong suit so I am not sure why the subsequent requests fail. Any thoughts would be appreciated.

 namespace TinyCLR_V1
{    class E_network
    {
      public static void Ethernet_Processing()
            
        {
            try

            {
                bool link_required = true;

                var networkController = GHIElectronics.TinyCLR.Devices.Network.NetworkController.FromName("GHIElectronics.TinyCLR.NativeApis.ENC28J60.NetworkController");

                var networkInterfaceSetting = new GHIElectronics.TinyCLR.Devices.Network.EthernetNetworkInterfaceSettings();

                var networkCommunicationInterfaceSettings = new GHIElectronics.TinyCLR.Devices.Network.SpiNetworkCommunicationInterfaceSettings();

                var cs = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin(GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.PD3);

                var settings = new GHIElectronics.TinyCLR.Devices.Spi.SpiConnectionSettings()
                {
                    ChipSelectLine = cs,
                    ClockFrequency = 4000000,
                    Mode = GHIElectronics.TinyCLR.Devices.Spi.SpiMode.Mode0,
                    ChipSelectType = GHIElectronics.TinyCLR.Devices.Spi.SpiChipSelectType.Gpio,
                    ChipSelectHoldTime = TimeSpan.FromTicks(10),
                    ChipSelectSetupTime = TimeSpan.FromTicks(10)
                };


                networkCommunicationInterfaceSettings.SpiApiName = GHIElectronics.TinyCLR.Pins.SC20100.SpiBus.Spi3;
                networkCommunicationInterfaceSettings.GpioApiName = GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.Id;

                networkCommunicationInterfaceSettings.SpiSettings = settings;
                networkCommunicationInterfaceSettings.InterruptPin = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin
                                                                    (GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.PC5);
                networkCommunicationInterfaceSettings.InterruptEdge = GpioPinEdge.FallingEdge;
                networkCommunicationInterfaceSettings.InterruptDriveMode = GpioPinDriveMode.InputPullUp;

                networkCommunicationInterfaceSettings.ResetPin = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin
                                                                (GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.PD4);
                networkCommunicationInterfaceSettings.ResetActiveState = GpioPinValue.Low;

                networkInterfaceSetting.Address = new System.Net.IPAddress(new byte[] { 10, 1, 1, 1 });
                networkInterfaceSetting.SubnetMask = new System.Net.IPAddress(new byte[] { 255, 255, 255, 0 });
               // networkInterfaceSetting.GatewayAddress = new System.Net.IPAddress(new byte[] { 10, 1, 1, 1 });
               // networkInterfaceSetting.DnsAddresses = new System.Net.IPAddress[] { new System.Net.IPAddress(new byte[]
               // { 75, 75, 75, 75 }), new System.Net.IPAddress(new byte[] { 75, 75, 75, 76 }) };

                networkInterfaceSetting.MacAddress = new byte[] { 0xEC, 0xAD, 0xB8, 0x74, 0xF1, 0x7C };
                networkInterfaceSetting.IsDhcpEnabled = false;
                networkInterfaceSetting.IsDynamicDnsEnabled = false;

                networkInterfaceSetting.TlsEntropy = new byte[] { 0, 1, 2, 3 };

                networkController.SetInterfaceSettings(networkInterfaceSetting);
                networkController.SetCommunicationInterfaceSettings(networkCommunicationInterfaceSettings);

                networkController.SetAsDefaultController();

                networkController.NetworkAddressChanged += NetworkController_NetworkAddressChanged;
                networkController.NetworkLinkConnectedChanged += NetworkController_NetworkLinkConnectedChanged;

                networkController.Enable();
                System.Diagnostics.Debug.WriteLine("Network is ready to use:   ");

                Thread.Sleep(100);

                //Modbus Device Configuration

                GHIElectronics.TinyCLR.Devices.Modbus.ModbusDevice ModbusTCP_Device;
                ModbusTCP_Device = new MyModbusDevice(248);

                GHIElectronics.TinyCLR.Devices.Modbus.Interface.ModbusTcpListener mbListner;
                mbListner = new GHIElectronics.TinyCLR.Devices.Modbus.Interface.ModbusTcpListener(ModbusTCP_Device, 502, 5, 1000);
                Thread.Sleep(100);
                ModbusTCP_Device.Start();
                                

                while (link_required == true)
                {
                    
                    if (ModbusTCP_Device.IsRunning == true)
                    {
                         //      System.Diagnostics.Debug.WriteLine("Running   ");

                    }
                    else
                    {
                        System.Diagnostics.Debug.WriteLine("Modbus Devic is Stopped ");
                    }
                    
                    Thread.Sleep(1000);

                }

            }

              catch
              {
                Debug.WriteLine("Error in E_Network");
              }


        }

        private static void NetworkController_NetworkLinkConnectedChanged(NetworkController sender, NetworkLinkConnectedChangedEventArgs e)
        {
            Debug.WriteLine("connection changed");
         
            // Raise event connect/disconnect
        }

        private static void NetworkController_NetworkAddressChanged(NetworkController sender, NetworkAddressChangedEventArgs e)
        {
           var ipProperties = sender.GetIPProperties();
           var address = ipProperties.Address.GetAddressBytes();

           // linkReady = address[0] != 0;
        }
    }


// implement slave device
public class MyModbusDevice : ModbusDevice

{
        
    public MyModbusDevice(byte deviceAddress, object syncObject = null)
       : base(deviceAddress, syncObject)
    { }

    public MyModbusDevice(IModbusInterface intf, byte deviceAddress, object syncObject = null)
       : base(intf, deviceAddress, syncObject)
    { }

    protected override string OnGetDeviceIdentification(ModbusObjectId objectId)
    {
        switch (objectId)
        {
            case ModbusObjectId.VendorName:
                return "Vendor Name";
            case ModbusObjectId.ProductCode:
                return "101";
            case ModbusObjectId.MajorMinorRevision:
                return "1.0";
            case ModbusObjectId.VendorUrl:
                return "www.test.com.au";
            case ModbusObjectId.ProductName:
                return "Test Modbus";
            case ModbusObjectId.ModelName:
                return "1";
            case ModbusObjectId.UserApplicationName:
                return "SC100";
        }
        return null;
    }

    protected override ModbusConformityLevel GetConformityLevel()
    {
        return ModbusConformityLevel.Regular;
    }


     
    protected override ModbusErrorCode OnReadHoldingRegisters(bool isBroadcast, ushort startAddress, ushort[] registers)
    {       
        try
        {
            for (int n = 0; n < registers.Length; ++n)
            {
            // just set a number in each register to test
            registers[n] = 2;
             }
         
            System.Diagnostics.Debug.WriteLine("Read Response Fired");
            
            return ModbusErrorCode.NoError;
        }

        catch 
        {
            Debug.WriteLine("error in on read holding registers");
            return base.OnReadHoldingRegisters(isBroadcast, startAddress, registers);
        }

    }

 

    // override any On<ModusFunction> methods here
}

Hi, can you please post your main() method?

Main method:

using System;
using System.Collections;
using System.Text;
using System.Threading;
 
namespace TinyCLR_V1
{
      
    class Program
    {
   
   
        static void Main()
        {
            
         var Ethernet_Thread = new Thread(E_network.Ethernet_Processing);
         Ethernet_Thread.Start();
           
            bool system_required = true;
            while(system_required == true)
            {
                Thread.Sleep(200);

            }

        }
    }
}

Perhaps the E_network.Ethernet_Processing method, then :wink:

Hi, it seems still to complicated for me to make a compilable application from the code snippets. Perhaps you should provide a download link to get your complete application. On a frist glance I wouldn’t use try catch blocks over big code segements. I would try to eliminate the try/catch blocks and set breakpoints to see how far you get,

This Link has the complete solution if that helps.

TG

1 Like

I think that there are some ‘unusual’ things in your code. For example I do not understand why you declare and initialize the network controller in a second thread and not in the main thread and main class instead of passing a reference to the E_network class?

I have modified the code so there is only a single thread and hopefully have removed anything that is “unusual”. I get the same results.
Test tool reads the holding registers once.
Every read attempt after that results in a timeout.
The “onreadholdingregisters”:
Fires once only on the first successful read.
Does not fire on subsequent read requests
Main loop continues to process.

using System;
using System.Diagnostics;
using System.Threading;
using GHIElectronics.TinyCLR.Devices.Gpio;
namespace TinyCLR_V1
{
  
     
    class Program
    {
     static void Main()
        {
            
            var networkController = GHIElectronics.TinyCLR.Devices.Network.NetworkController.FromName("GHIElectronics.TinyCLR.NativeApis.ENC28J60.NetworkController");

            var networkInterfaceSetting = new GHIElectronics.TinyCLR.Devices.Network.EthernetNetworkInterfaceSettings();

            var networkCommunicationInterfaceSettings = new GHIElectronics.TinyCLR.Devices.Network.SpiNetworkCommunicationInterfaceSettings();

            var cs = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin(GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.PD3);

            var settings = new GHIElectronics.TinyCLR.Devices.Spi.SpiConnectionSettings()
            {
                ChipSelectLine = cs,
                ClockFrequency = 4000000,
                Mode = GHIElectronics.TinyCLR.Devices.Spi.SpiMode.Mode0,
                ChipSelectType = GHIElectronics.TinyCLR.Devices.Spi.SpiChipSelectType.Gpio,
                ChipSelectHoldTime = TimeSpan.FromTicks(10),
                ChipSelectSetupTime = TimeSpan.FromTicks(10)
            };


            networkCommunicationInterfaceSettings.SpiApiName = GHIElectronics.TinyCLR.Pins.SC20100.SpiBus.Spi3;
            networkCommunicationInterfaceSettings.GpioApiName = GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.Id;

            networkCommunicationInterfaceSettings.SpiSettings = settings;
            networkCommunicationInterfaceSettings.InterruptPin = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin
                                                                (GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.PC5);
            networkCommunicationInterfaceSettings.InterruptEdge = GpioPinEdge.FallingEdge;
            networkCommunicationInterfaceSettings.InterruptDriveMode = GpioPinDriveMode.InputPullUp;

            networkCommunicationInterfaceSettings.ResetPin = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin
                                                            (GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.PD4);
            networkCommunicationInterfaceSettings.ResetActiveState = GpioPinValue.Low;

            networkInterfaceSetting.Address = new System.Net.IPAddress(new byte[] { 10, 1, 1, 1 });
            networkInterfaceSetting.SubnetMask = new System.Net.IPAddress(new byte[] { 255, 255, 255, 0 });
            // networkInterfaceSetting.GatewayAddress = new System.Net.IPAddress(new byte[] { 10, 1, 1, 1 });
            // networkInterfaceSetting.DnsAddresses = new System.Net.IPAddress[] { new System.Net.IPAddress(new byte[]
            // { 75, 75, 75, 75 }), new System.Net.IPAddress(new byte[] { 75, 75, 75, 76 }) };

            networkInterfaceSetting.MacAddress = new byte[] { 0xEC, 0xAD, 0xB8, 0x74, 0xF1, 0x7C };
            networkInterfaceSetting.IsDhcpEnabled = false;
            networkInterfaceSetting.IsDynamicDnsEnabled = false;

            networkInterfaceSetting.TlsEntropy = new byte[] { 0, 1, 2, 3 };

            networkController.SetInterfaceSettings(networkInterfaceSetting);
            networkController.SetCommunicationInterfaceSettings(networkCommunicationInterfaceSettings);

            networkController.SetAsDefaultController();

            networkController.Enable();
            System.Diagnostics.Debug.WriteLine("Network is ready to use:   ");

            Thread.Sleep(100);

            //Modbus Device Configuration

            GHIElectronics.TinyCLR.Devices.Modbus.ModbusDevice ModbusTCP_Device;
            ModbusTCP_Device = new MyModbusDevice(248);

            GHIElectronics.TinyCLR.Devices.Modbus.Interface.ModbusTcpListener mbListner;
            mbListner = new GHIElectronics.TinyCLR.Devices.Modbus.Interface.ModbusTcpListener(ModbusTCP_Device, 502, 5, 1000);
            Thread.Sleep(100);
            ModbusTCP_Device.Start();

            bool system_required = true;
            string debug_string = "---";
            Int32 system_counter = 0;
            while(system_required == true)
            {
                system_counter ++;
                debug_string ="System Counter - "+ system_counter.ToString();
                                
                Debug.WriteLine(debug_string);
                Thread.Sleep(5000);

            }

        }
    }
}

using System.Diagnostics;
using System.Threading;
using GHIElectronics.TinyCLR.Devices.Modbus;
using GHIElectronics.TinyCLR.Devices.Modbus.Interface;

// implement slave device
public class MyModbusDevice : ModbusDevice

{
        
    public MyModbusDevice(byte deviceAddress, object syncObject = null)
       : base(deviceAddress, syncObject)
    { }

    public MyModbusDevice(IModbusInterface intf, byte deviceAddress, object syncObject = null)
       : base(intf, deviceAddress, syncObject)
    { }

    protected override string OnGetDeviceIdentification(ModbusObjectId objectId)
    {
        switch (objectId)
        {
            case ModbusObjectId.VendorName:
                return "Vendor Name";
            case ModbusObjectId.ProductCode:
                return "101";
            case ModbusObjectId.MajorMinorRevision:
                return "1.0";
            case ModbusObjectId.VendorUrl:
                return "www.test.com.au";
            case ModbusObjectId.ProductName:
                return "Test Modbus";
            case ModbusObjectId.ModelName:
                return "1";
            case ModbusObjectId.UserApplicationName:
                return "SC100";
        }
        return null;
    }

    protected override ModbusConformityLevel GetConformityLevel()
    {
        return ModbusConformityLevel.Regular;
    }


     
    protected override ModbusErrorCode OnReadHoldingRegisters(bool isBroadcast, ushort startAddress, ushort[] registers)
    {       
        try
        {
            for (int n = 0; n < registers.Length; ++n)
            {
            // just set a number in each register to test
            registers[n] = 2;
             }
         
            System.Diagnostics.Debug.WriteLine("Read Response Fired");
            
            return ModbusErrorCode.NoError;
        }

        catch 
        {
            Debug.WriteLine("error in on read holding registers");
            return base.OnReadHoldingRegisters(isBroadcast, startAddress, registers);
        }

    }

 

    // override any On<ModusFunction> methods here
}

Complete modifies solution is here:

        https://www.dropbox.com/sh/uqpv0kra9kecag3/AABIjXOAOSahjmEgUQHmQuana?dl=0

Perhaps it may be helpful to include the code of the library as source code instead of using the nuget version.
-TinyCLR-Libraries/GHIElectronics.TinyCLR.Devices.Modbus at dev · ghi-electronics/TinyCLR-Libraries · GitHub

Thanks @RoSchmi I will delve a little deeper

Using your setup I can confirm your observation that only the first request is successful. Would be nice if one could debug the communication on the master side too. Some time ago I made a little modbus master tool for my own use in C#. Unfortunately it supports only serial port transmission.
-GitHub - RoSchmi/ModbusWindowsForm: Windows Forms App to test some Modbus functions

I have the Modbus Device functioning in that I can now continuously poll the “device” and receive
a valid response every time.

I am not suggesting how I have resolved this is the correct way only showing what I have found so that others who are far more experienced than I may suggest the best solution.

As suggested by @RoSchmi I removed the Modbus nuget version and loaded the modbus files directly into my project.

My understanding is currently when the device receives its first read it process this correctly but then proceeds to close the TCP connection - hence no more read responses. It does this as
intf.IsConnectionOK has a value of 0.

I have inverted the “if statement” and now the everything appears to function correctly.

The screen shot shows the original code of ModbusDevices.cs - I have simply remove the !.

So it now reads if (intf.IsConnectionOK) {

Great, that you found a fix. I’m sure that GHI will have a look on this, And thank you for your Modbus Slave example.

Can you please summarize the issue here or open an issue on GitHub?

Hi @Gus_Issa summary of my process / understanding is:

 A ModbusTCP Device was created.
 The device was configured to respond to "ReadHoldingRegisters".
 The first attempt by a Modbus Master to read holding registers from the device is successful however   
 subsequent reads result in a timeout.
 Testing was on a SC20100S Dev Rev C board with a EthClick Module - pos 1.

In the “Modbus device” library code after the read request is processed it then
looks at whether the interface connection is OK and if not closes and disposes
of the connection. This is always occurring after the first read - i.e. the connection is
closed (therefore no more read responses) as intf.IsConnectionOK = 0

I do not know if this code is incorrect or whether the problem is deeper.
As a very temporary measure I inverted the “IF” test logic and the device then
correctly responded to all read events. In a quick test 3000 read requests received
3000 valid read responses.

I am not a skilled C# programmer my knowledge is not at the level to required to properly
resolve this.

There is a link above to the full solution that shows this problem and this was kindly verified by @RoSchmi.

If there is anything else I can do or provide please just let me know.

1 Like

I had a look on the code of the library too. Seems that the “!” in the if-clause came in accidently.

I looked at the old NetMF code (which works) and it is the same as the TinyCLR Nuget version (ie it includes the !intf.IsConnectionOK) so I’m thinking the problem is deeper than just the If statement.

Yes, you are right.
I still do not understand the structure of the library. The property ‘IsConnectionOk’ of the Class ‘ModbusTcpInterface’ has only a get accessor. The state depends on two things: 1) socket = null, 2) property ‘IsSocketConnected’ = true/false. Nowhere in the code of the library is a place where something is written to the property ‘IsSocketConnected’. I can see no way, how this property can be accessed from outside.

Edit: Now I have seen a possible reason. In TinyClr the Class ‘ModbusTcpInterface’ and esp. the code behind ‘IsConnectionOk’ is different from the original NETMF Version.
-https://github.com/gremlinc5/ModbusLib/blob/master/Modbus/Interface/ModbusTcpInterface.cs

Can you please apply the change same as NetMF below and revert what you changed to see if the problem is fixed?

And, thank to @RoSchmi :))

public bool IsConnectionOk
      {
#if NETMF
         get
         {
            if (_Socket != null)
            {
               try
               {
                  bool error = _Socket.Poll(0, SelectMode.SelectError);
                  bool read = _Socket.Poll(0, SelectMode.SelectRead);
                  return !error &&
                     (!read || _Socket.Available > 0);
               }
               catch
               {
                  // ignored
               }
            }
            return false;
         }
#else
         get { return _Socket != null && _Socket.Connected; }
#endif

Hello Dat,
I changed the code in ‘ModbusTcpInterface.cs’ to:

public bool IsConnectionOk
    {
        get
        {
            if (this.socket != null)
            {
                try
                {
                    bool error = this.socket.Poll(0, SelectMode.SelectError);
                    bool read = this.socket.Poll(0, SelectMode.SelectRead);
                    return !error &&
                       (!read || this.socket.Available > 0);
                }
                catch
                {
                    // ignored
                }
            }
            return false;
        }
    }

I left the code in ‘ModbusDevice’ as it was in the NuGet version.

The Modbus slave resonded to all requests from the master.

When I pulled the ethernet cable while succeeding transmissions were running, the transmissions stopped (as expected :wink:), the App on the ModbusDevice didn’t crash (I waited for 3 minutes) and when i restored the ethernet connection, the transmissions continued to work without any further action. This looks quite good as I think. What I do not know is: in wich case is it intended to disose the socket?

1 Like