Home / C# / HMI with C# and WPF part 1: getting started with MVVM and plc communication

HMI with C# and WPF part 1: getting started with MVVM and plc communication

This article is part of a series that explains how to write an HMI with C# and WPF.
Part 1: this article
Part 2: Create multiple pages and navigation with PRISM

In this article we will see how to create from scratch a simple HMI software, which communicates to a Siemens S7 plc.
We will use WPF and MVVM pattern.
We will also see an implementation of a PLC driver in a service and how to consume the service from the User Interface.

We are going to do:

  • Create a simple UI to show the state of a process
  • Download a MVVM framework to help us implement the MVVM pattern
  • Create the ViewModel associated to the UI
  • Create the PLC service to connect to the plc by using Sharp7 driver

Check out the video on Youtube

Create the View

Usually when I create a new program, I start from the User Interface (UI). Thanks to this I can concentrate more on the functionality I need, then move on to the implementation of the low level functionalities.

Here is the UI that we will work with:

You can take a look at the XAML code to create the UI here.

Choosing an MVVM framework

It’s also helpful to use an MVVM framework, which will save us the effort of writing the boilerplate code to implement all the functionalities. There are lot of frameworks out there, but the two that I use the most are MVVM Light and PRISM.

The two frameworks have a lot of differences, but for this application it’s easy to replace one with the other. In this article I will use PRISM, but in the video I used MVVM light.

To download Prism you should right click on the WPF project, go on Manage NuGet packages, then Browse, search for Prism.WPF, click Install.

Create the ViewModel

To apply MVVM pattern, we need to create a ViewModel for every View that we have. In this project we have only one View (MainWindow), so we will create only one ViewModel (MainWindowViewModel).
MVVM implies that for every View you should have its own dedicated ViewModel.

Since we started from the UI, we already know all the properties and commands that our ViewModel should contain.

These are the properties:

  • IpAddress
  • HighLimit
  • LowLimit
  • PumpState
  • TankLevel
  • ConnectionState
  • ScanTime

These are the commands:

  • ConnectCommand
  • DisconnectCommand
  • StartCommand
  • StopCommand

Here is an example of how to declare a property, depending on the MVVM framework you are using (PRISM in this example):

public string IpAddress
{
    get { return _ipAddress; }
    set { SetProperty(ref _ipAddress, value); }
}
private string _ipAddress;

And here is an example on how to declare a command. WPF commands are usually DelegateCommands (PRISM) or RelayCommands (MVVM light), and they expose an Action (that is a method that returns void) to be executed when the command is invoked.

To create a command we have to take 3 steps:

1) Declaration of the command:

public ICommand ConnectCommand { get; private set; }

2) Create the action to be executed, which is just a method:

private void Connect()
{
    _plcService.Connect(IpAddress, 0, 1);
}

3) In the viewmodel, inside the constructor, you have to create the Command instance and pass the Action as a parameter.

ConnectCommand = new DelegateCommand(Connect);

You can read the properties and commands implementation in this file: https://github.com/mesta1/Simple-HMI-with-WPF/blob/master/SimpleHmi/ViewModels/MainWindowViewModel.cs

Binding the View to the ViewModel

To connect the View to the ViewModel, we have to assign to the DataContext of the View an instance of the ViewModel.
The two most common ways to do this are:

1) XAML: when opening the page an instance of the ViewModel is created automatically. This is the way that we use in this example.

<Window.DataContext>
    <vm:MainWindowViewModel />
</Window.DataContext>

2) Code-behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainWindowViewModel();
    }
}

In this project we go with the first approach, for simplicity, but when we will add navigation we will use another approach built-in in the framework that we use. This can be either by a ViewModel locator, or by using DataTemplates. You can read this post if you are interested.

Once we bound the View to the ViewModel, we can assign the Binding and the Commands to our UI elements.
This can be done either by writing them down directly in xaml, or by using the UI of Visual Studio, in the Properties tab.

This will output the following XAML, for a Textblock:

<TextBox Text="{Binding IpAddress}" />

Connecting the PLC values to the ViewModel values

Now that our UI is connected to our ViewModel, it’s time to update our ViewModel data from the plc.
To do this we can create a Service (in the example S7PlcService), which is responsible for providing values from the Plc and for executing the commands to the Plc.
It’s the PlcService that contains the plc driver, in this case Sharp7 driver (you can read more about it here).

The reponsibilities of PlcService are:
1) to execute the commands
2) to provide the most actual values from the plc

In order to do this we have to implement:
1) The Connect and Disconnect command
2) The Start and Stop command
3) A timer to refresh the values from the plc constantly
4) An event to notify the consumers of the service (in this case it’s just the ViewModel) that we updated the values and they should be refreshed

We also need to handle somehow the connection errors, with some sort of “Retry” mechanism and alarm logging. This is out of scope of this article. I will talk about it in a future article, when I will extend the application. To handle the errors in this example, I just write them on the “Output” of Visual Studio.

You can read the full implementation of PlcService here.

Read and refresh values

The timer that I used is a System.Timers.Timer. This timer is not executed in the UI thread and will not block the UI when it requests data from the plc.
Notice that the timer is defined once during the construction of the PlcService, and it will last for all the life of the service itself, that usually is the same as the life of the application.
Another feature of System.Timers.Timer is to fire events exactly once every interval, instead of waiting for the complete execution of the callback.
That’s why I call Stop() and then Start() in the OnTimerElapsed callback.
Here is the timer definition:

private readonly System.Timers.Timer _timer;

public S7PlcService()
{
    _timer = new System.Timers.Timer();
    _timer.Interval = 100;
    _timer.Elapsed += OnTimerElapsed;	
}

private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
    try
    {
        _timer.Stop();
        ScanTime = DateTime.Now - _lastScanTime;
        RefreshValues();
        OnValuesRefreshed();
    }
    finally
    {
        _timer.Start();
    }
    _lastScanTime = DateTime.Now;
}

To read the values I just use the Sharp7 functions to read a DB. Notice the “lock (_locker)” to prevent access to the client from different threads. We will talk about this later when we will write the values.
Here is the code to read the values:

private void RefreshValues()
{
    lock (_locker)
    {
        var buffer = new byte[4];
        int result = _client.DBRead(1, 0, buffer.Length, buffer);
        if (result == 0)
        {
            PumpState = S7.GetBitAt(buffer, 0, 2);
            HighLimit = S7.GetBitAt(buffer, 0, 3);
            LowLimit = S7.GetBitAt(buffer, 0, 4);
            TankLevel = S7.GetIntAt(buffer, 2);
        }
        else
        {
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "\t Read error: " + _client.ErrorText(result));
        }
    }
}

Once the Refresh of the values has been executed, it is important to notify the consumer of the services that the values were changed through an event, in this case ValuesRefreshed event.

 private void OnValuesRefreshed()
{
    ValuesRefreshed?.Invoke(this, new EventArgs());
}

Defining commands and writing values

Commands that have to be executed on the plc usually imply that you have to write some values on the CPU. The difference between the read, that is asynchronous from the UI, is that the commands are called in the same thread of the UI.
This may cause 2 problems:
1) it may happen that we write while we are reading
2) we may freeze the UI while we write

To overcome the first problem we can use a “lock” mechanism. This will prevent the access to the shared variables (in this case it’s the “_client”) from different threads.
To solve the second problem, we have to be sure that the duration of the command is really short or is asynchronous.

Short commands in this case are Connect and Disconnect. They just open and close the TCP socket (inside the client) and send some data to initialize the communication.
Also connect and disconnect are responsible for starting and stopping the timer for refreshing the values.
This is the implementation of Connect and Disconnect:

public void Connect(string ipAddress, int rack, int slot)
{
    try
    {
        ConnectionState = ConnectionStates.Connecting;
        int result = _client.ConnectTo(ipAddress, rack, slot);
        if (result == 0)
        {
            ConnectionState = ConnectionStates.Online;
            _timer.Start();
        }
        else
        {
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "\t Connection error: " + _client.ErrorText(result));
            ConnectionState = ConnectionStates.Offline;
        }
        OnValuesRefreshed();
    }
    catch
    {
        ConnectionState = ConnectionStates.Offline;
        OnValuesRefreshed();
        throw;
    }
}

public void Disconnect()
{
    if (_client.Connected)
    {
        _timer.Stop();
        _client.Disconnect();
        ConnectionState = ConnectionStates.Offline;
        OnValuesRefreshed();
    }
}

The writes of the push buttons are long lasting commands, since we need to write “true”, then Sleep for some milliseconds, then write “false”. In case of a retry mechanism we also have to wait for retries.
For this commands we use the async/await keywords of .Net framework. Basically we wrap the two writes and the Sleep in a Task, then we mark the method “async” and we await the task to be executed.
Here is the implementation of “WriteStart”:

public async Task WriteStart()
{
    await Task.Run(() =>
    {
        int writeResult = WriteBit("DB1.DBX0.0", true);
        if (writeResult != 0)
        {
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "\t Write error: " + _client.ErrorText(writeResult));
        }
        Thread.Sleep(30);
        writeResult = WriteBit("DB1.DBX0.0", false);
        if (writeResult != 0)
        {
            Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "\t Write error: " + _client.ErrorText(writeResult));
        }
    });
}	

Download the sample code

As usual, you can download the sample code together with the plc program on Github.

Share Button

3 comments

  1. Thank you for this cool tutorial – for some reason I’m not able to connect to a real Siemens PLC (S7 1516F TIA V14)
    Are there some restrictions?

    I also created an DB1 with absolut adressing (DB1.DBx0.0 etc.)

    Thanks in advance

Leave a Reply