Home / C# / HMI with C# and WPF part 3: Communicating with multiple plc (S7 and Modbus)

HMI with C# and WPF part 3: Communicating with multiple plc (S7 and Modbus)

In the previous articles of this series we saw how to communicate with a single Siemens S7 plc.

But it happens frequently to have to communicate with multiple PLCs.

In that case, our HMI has to read and aggregate the data that are coming from the field and to react/notify the users in case of malfunctioning of certain areas.

One of the biggest challenges in designing such applications is to maintain a decoupled and flexible architecture, so it can scale quickly as the complexity of the machine/plant grows.

In this article we will see how PRISM and dependency injection will help us managing multiple PLCs in a single application.
This article is part of a series, you can read the previous articles here:

Watch the video on YouTube

Requirements

The requirements that we are going to implement in the HMI are:

  • Read and write data from another PLC using Modbus protocol.
  • In the new plc, we have to read a boolean value and set/reset it with a button
  • Show the connection state of both PLC in the StatusBar
  • Show a page with the data from the second plc

Here is the network topology:

And here is the new User Interface, where we add the new Page and the new fields in the StatusBar:

Implementing the UI

As usual, when we have to add new features, we start from the UI.
Since we are using MVVM pattern, we can create a Plc2MainPage and a Plc2MainPageViewModel.
The Plc2MainPage consists only in a button and a led that represent the state of the variable that we read/write on the plc. You can check the code here.

The Plc2MainPageViewModel must implement the new properties that we are going to refresh from the plc.
It also has to implement the command to Write the variable on the Plc.
Here is the code:

public bool FirstOutput
{
    get { return _firstOutput; }
    set { SetProperty(ref _firstOutput, value); }
}
private bool _firstOutput;

public ICommand WriteFirstOutputCommand { get; private set; }

public Plc2MainPageViewModel()
{
    WriteFirstOutputCommand = new DelegateCommand(WriteFirstOutput);
}

private void WriteFirstOutput()
{
   // Here we need a service to write to the plc
}

Create the Plc2 service

Now that we created the UI and we have a clear understanding on what functionalities we need from the plc2 service, we can create the service.
To do this, we have to create a new project and name it SimpleHmi.Plc2Service.

Then we have to add a Modbus driver. In this project we will use NModbus4, so we can use NuGet and browse for it.
To access on NuGet just right click on the project name, click on “Manage NuGet Packages”, then follow these instructions:

As we did for the S7 service, we will create an interface that defines the properties and the methods that the service will offer. Then we will also create the real implementation of the service by using NModbus.

The IPlc2Service interface contains all the methods to manage the communication with the plc, like Connect, Disconnect, ConnectionState, ScanTime and the ValuesRefreshed event.
It also contains all the PLC-related values, in this case the boolean variable that we need to read and the method to write it.
Here is the code of IPlc2Service interface:

public interface IPlc2Service
{
    ConnectionStates ConnectionState { get; }

    TimeSpan ScanTime { get; }

    bool FirstOutput { get; }

    event EventHandler ValuesRefreshed;

    void Connect();
    void Disconnect();

    Task WriteFirstOutput(bool value);
}

To create the real Plc2 service we create a class ModbusPlc2Service. If you are not familiar with NModbus, you can read an article here.
In this class we use the same approach that we used with the S7 service. We use a multithreaded timer (System.Timers.Timer) that ticks once every 100 ms.
The Connect and Disconnect methods will be responsible for creating the socket and starting the Timer, and when the Timer ticks we read the variable in the callback.
We also implement the Write method by using a Task, so we can execute it asynchronously from the UI thread.
You can read the code of Plc2Service here.

Where to put the common classes

One of the problems that we face when creating the IPlc2Service is that it needs the type ConnectionStates.
ConnectionStates is part of Plc1Service, which has nothing in common with Plc2Service, except for this class.
With PRISM it’s convenient to put all the Common components in a dedicated project called Infrastructure, and reference this project from the other projects.
By putting all the common parts in the same assembly, we obtain a cleaner dependency chart and decoupled components.

You can read the Infrastructure classes and project here.

Register the Plc2 service and consume it in the UI

To use the ModbusPlc2Service in our application, we have first to register it in the container.
To do this we have to open the bootstrapper class and register it as SingleInstance, same as we did with the S7 plc service.

protected override void ConfigureContainerBuilder(ContainerBuilder builder)
{
    base.ConfigureContainerBuilder(builder);
    // Registers the S7 plc service
    builder.RegisterType<S7PlcService>().As<IPlcService>().SingleInstance();    

    // Registers the Modbus plc service   
    builder.RegisterType<ModbusPlc2Service>().As<IPlc2Service>().SingleInstance();
    
	...
}  

After registering the service, we can use it in Plc2MainPageViewModel, just by using Dependency Injection in the constructor.
To do that, we have to create a private read-only variable to reference the service. Then we can subscribe to the ValuesRefreshed event to update our UI variable.

private readonly IPlc2Service _plc2Service;

public Plc2MainPageViewModel(IPlc2Service plc2Service)
{
    _plc2Service = plc2Service;
    _plc2Service.ValuesRefreshed += OnPlc2Service_ValuesRefreshed;
	...
}
		
private void OnPlc2Service_ValuesRefreshed(object sender, EventArgs e)
{
    FirstOutput = _plc2Service.FirstOutput;
}

We can also implement the action for the Write Command that we wrote before.

private void WriteFirstOutput()
{
    _plc2Service.WriteFirstOutput(!FirstOutput);
}

Modify the StatusBar to show data from two PLCs

In the previous articles, we were displaying in the StatusBar if the Plc1 was Online/Offline and the PLC scan time.
Now that we are reading from another plc, we need to show also the connection state and the scan time of the Plc2.
To do that, we modify the UI of the StatusBar to add two more fields. You can read the xaml code here.

Then we have to update the HmiStatusBarViewModel to refresh the UI values.
Same as before, we will use Dependency Injection to get our services from the constructor, so we can subscribe to both ValuesRefreshed events.

public HmiStatusBarViewModel(IPlcService plcService, IPlc2Service plc2Service)
{
    _plcService = plcService;
    _plc2Service = plc2Service;

    _plcService.ValuesRefreshed += OnPlcServiceValuesRefreshed;
    OnPlcServiceValuesRefreshed(null, EventArgs.Empty);

    plc2Service.ValuesRefreshed += OnPlc2ServiceValuesRefreshed;
    OnPlc2ServiceValuesRefreshed(null, EventArgs.Empty);
}
		
private void OnPlc2ServiceValuesRefreshed(object sender, EventArgs e)
{
    Plc2ConnectionState = _plc2Service.ConnectionState;
    Plc2ScanTime = (int)_plc2Service.ScanTime.TotalMilliseconds;
}

private void OnPlcServiceValuesRefreshed(object sender, EventArgs e)
{
    Plc1ConnectionState = _plcService.ConnectionState;
    Plc1ScanTime = _plcService.ScanTime;
}

Autoconnect to the plc at application startup

In the previous application, we were connecting to the plc by adding the IP address and clicking “Connect” button.
This was not optimal, because our application should be able to auto-connect to the PLC at startup, especially when communicating with multiple PLCs.
To implement the auto-connection, we have to change the Connect methods to remove all the parameters.
And to keep things simple we will hardcode the ip address and other parameters in the Plc service.

Here is the connect code for S7PlcService:

public void Connect()
{
    try
    {
        ConnectionState = ConnectionStates.Connecting;
		// Ip address, rack and slot are hardcoded now
        int result = _client.ConnectTo("127.0.0.1", 0, 1); 
        if (result == 0)
        {
            ConnectionState = ConnectionStates.Online;
            _timer.Start();
        }
        else
        {
           ...
		}
	}
}

and here is the code for ModbusPlc2Service, which is slightly different due to NModbus implementation:

public void Connect()
{
    ConnectionState = ConnectionStates.Connecting;
    _client = new TcpClient("127.0.0.1", 502);
    _master = ModbusIpMaster.CreateIp(_client);
    ConnectionState = ConnectionStates.Online;
    _timer.Start();
}

At last, we have to call the Connect method at the application startup and Disconnect at the application shutdown.
We can do this in the MainWindowViewModel class, again by using Dependency Injection in the constructor to resolve the two plc services.
Then we have to create two commands: ConnectPlcCommand and DisconnectPlcCommand.
Here is the ViewModel code:

public ICommand ConnectPlcCommand { get; private set; }
public ICommand DisconnectPlcCommand { get; private set; }
	
public MainWindowViewModel(IRegionManager regionManager, IPlcService plcService, IPlc2Service plc2Service)
{  
    _plcService = plcService;
    _plc2Service = plc2Service;  
    ...   
	ConnectPlcCommand = new DelegateCommand(ConnectPlc);
    DisconnectPlcCommand = new DelegateCommand(DisconnectPlc);
}

private void ConnectPlc()
{
    _plcService.Connect();
    _plc2Service.Connect();
}

private void DisconnectPlc()
{
    _plcService.Disconnect();
    _plc2Service.Disconnect();
}

Now to call these commands from the Loaded and Closing event of the MainWindow, we have to use Triggers and Interactivity.

We have to download the Expression.Blend.Sdk from NuGet packages, just as shown in the picture:

And in MainWindow we have to write two EventTriggers, one for the Loaded event and one for the Closing event, and to call the Commands with a InvokeCommandAction:

<Window x:Class="SimpleHmi.Views.MainWindow"
        ...
		xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        mc:Ignorable="d" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding Path=ConnectPlcCommand, Mode=OneWay}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding Path=DisconnectPlcCommand, Mode=OneWay}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

Writing a DummyPlc2Service

This application has also a dummy Plc2Service so you can launch it even if you don’t have the real hardware.
To change the type of service, you can go on bootstrapper class and comment/uncomment the service implementation that you need.

Download the sample code

You can find the source code on my GitHub repository.

6 comments

  1. Can yu share yur rmail here?

  2. Sometimes i trouble with problems.Because of this i nneed to ask something.Other things do you have knowledge abut mbus communication with c#?

  3. Hi; with raspberry; i follow you’re tuto but on this rpi and Win10, only UWP’s apps is supported. Can i converte you’re WPF app with software Microsoft to UWP app ?? Thank lot for you’re answer.

Leave a Reply

Share19
Tweet
Share
+1