TwinCAT HMI Server Extension

Java script is used heavily in TwinCAT HMI development. What will happen if something can’t be implemented by javascript or if we want to isolate our business logic in remote servers and there it is not possible to implement by javascript. TwinCAT HMI provides a way to implement the functionalities and use it as it has been a built-in type.  Properties and methods can be configured when a button has been clicked for example. The framework provides the mechanism so that when the button in UI is pressed then a method from the extension is called.

The development of server extensions is done in C#. The development takes place in a separate project type within the Visual Studio. A corresponding project template is available for this purpose.

This extension is a DLL that implements an interface provided by the HMI framework.  HMI can call methods implemented by the custom extension module. So if we try to call the method from a random DLL that will not be possible since the framework has no idea about the DLL.

Custom extension and TwinCAT architecture

Relation between TwinCAT HMI client and custom module

We shall show how to build the C# extension module (a C# DLL that will fit into the HMI framework.) The DLL can call methods from our fictitious home automation server over the internet. We shall add a  YouTube video here later.

Steps for building extension in C#

The module we are developing will read data from our home automation OPC UA server. The OPC UA server that we have developed in other module will be used here. We shall call method from the server by using this extension module. We shall have normal edit box in the TwinCAT HMI to show status. We shall verify the status by using  UA Expert.

STEP 01: In Visual Studio, select,  TwinCAT HMI | Extensibility | TwinCAT HMI Server Extension (CSharp)


STEP 02: Select the HMI version if you have multiple version install and then press Create

At this point, we have created the project and in visual studio it will show something similar.

Now we should be able to build the project successfully.  Browse the generated code and you can find the OnRequest method we have switch case with mapping case “RandomValue”  and “MaxRandom”. We should be able to get these random values from extension to UI if we want. To make it a bit useful to our needs, we need to change these or add some switch cases. Under the config folder, we have xx.Config.json.  We can edit the symbols and $ref types to fit our needs. We edit the C# file as needed. 

STEP 03: If the project was compiled successfully, the extension can be loaded from the server. Open the context menu in the TwinCAT HMI project under Server/Extensions. Select the Server | Extension | Mouse right-click | Manage Extension | Select the extension and press OK

STEP 04: Go to TwinCAT HMI Server Configuration and check if our extension is visible along with other built-in extensions.

STEP 05: We can map those variables so we can display these on the edit box for example.

 

We have modified the main source code in a way that status or Main Door and function status will be unknown by default. When the actual function call will be done then the real status will be updated. Here is the modified code.

                        // Use the mapping to check which command is requested
                        switch (mapping)
                        {
                            case "MainDoorStatus":
                                ret = GetMainDoorStatus(command);
                                break;
                            case "FunctionCallStatus":
                                ret = FunctionCallStatus(command);
                                break;
                            default:
                                ret = ErrorValue.HMI_E_EXTENSION;
                                break;
                        }
                     ...
         // Retrive main door status from the OPC UA Server
        private ErrorValue GetMainDoorStatus(Command command)
        {
            command.ReadValue = "Unknown";
            //Call OPC method here
            command.ExtensionResult = ExtensionErrorValue.HMI_EXT_SUCCESS;
            return ErrorValue.HMI_SUCCESS;
        }
        // Gets or sets the maximum random value.
        private ErrorValue FunctionCallStatus(Command command)
        {
            if (command.WriteValue.IsSet && command.WriteValue.Type == TcHmiSrv.ValueType.ValueType_String)
            {
                //Make OPC call here
            }
            command.ReadValue = "Unknown";
            command.ExtensionResult = ExtensionErrorValue.HMI_EXT_SUCCESS;
            return ErrorValue.HMI_SUCCESS;
        }

Symbol access right from the server extension is as follows:

We create variables in the extension with read and read-write mode by JavaScript file.  The JavaScript file is stored in the config folder of the extension project.  Here is the example JSON text from the config file.  ComboString is created  with READ access and ComboIndex is created with READ-WRITE mode. 


{
  "$schema": "ExtensionSettings.Schema.json",
  "guid": "a375d3c8-17bb-4a0a-994e-5947014cddaf",
  "version": "1.0.0.0",
  "configVersion": "1.0.0.79",
  "symbols": {
    "ComboString": {
      "readValue": {
        "allOf": [
          {
            "$ref": "tchmi:general#/definitions/String"
          },
          {
            "readOnly": true
          }
        ]
      },
      "hidden": false
    },
    "ComboIndex": {
      "readValue": {
        "$ref": "tchmi:general#/definitions/Integer"
      },
      "hidden": false
    }
  }
}

These information is translated to  HMI  SERVER extension configuration by the visual studio to another configuration. This configuration is   

project_Name_\Server\Extensions\TcHmiSrv\TcHmiSrv.Config.default

Related content of the file:

        "ServerExtensionCSharp1.ComboIndex": {
            "ACCESS": 3,
            "DOMAIN": "ServerExtensionCSharp1",
            "HIDDEN": false,
            "MAPPING": "ComboIndex",
            "OPTIONS": {},
            "SCHEMA": {
                "$ref": "tchmi:general#/definitions/Integer"
            },
            "USEMAPPING": true
        },
        "ServerExtensionCSharp1.ComboString": {
            "ACCESS": 1,
            "DOMAIN": "ServerExtensionCSharp1",
            "HIDDEN": false,
            "MAPPING": "ComboString",
            "OPTIONS": {},
            "SCHEMA": {
                "allOf": [
                    {
                        "$ref": "tchmi:general#/definitions/String"
                    },
                    {
                        "readOnly": true
                    }
                ]
            },
            "USEMAPPING": true
        },

If we want to write data to the variable ComboString from UI, it is not possible as the ACCESS is 1 on the other hand, writing to ComboIndex is possible as it has ACESS value 3.  With access right 1, data can flow from extension to UI but not in other direction. But with access right 3, data can flow back and forth.

 

Info is available at https://infosys.beckhoff.com/english.php?content=../content/1033/te2000_tc3_hmi_engineering/5627258891.html&id=

 

Following the final version of the HMI. User can press Open/Close door and the status of the call will be displayed. If the door status is changed then it will be updated in the door status edit box.

Tips: Sometime the default data in the extension is not visible in the UI. See the tips at 

https://www.hemelix.com/scada-hmi/beckhoff-hmi/beckhoff-hmi-symbol/

We need to establish the bridge between extension dll to opc call (that is missing). We shall make a YouTube video and publish here, so please do visit us again.

Tips:

Tips 01: When we are creating the module (DLL by the wizards provided by Beckhoff) we have seen it creates RandomValue  and MaxRandom. If we search by visual studio then replace all found items but in next time if you build the whole project then it will appear again. The JSON file is auto created by taking the data from HMI configuration (mapped symbol). If you un mapped the  symbol the it will be removed from the JSON file.

Tips 02: When changing variable info or updating extension DLL for the framework then we may see there is config error for the extension. This will disappear if we just just restart the server (from bottom right  corner).

Tips 03: Sometimes, the extension module is not updated. The reason for some reason the Visual Studio does not clean old files properly. As a result, you may need to clean it manually.

Tips 04: Symbols from extension module is shown or listed in the mapped list according to  TcHmiSrv.Config.default.json file (found in $ProjectPath\Server\Extensions\TcHmiSrv)

Imported libraries are located in the folder C:\TwinCAT\Functions\TF2000-HMI-Server\ServerExtensionCSharp1 when is our extension project. In the above image, we have used those DLL in our project  and those are visible in the exported project folder.

Tips 05: We need to make sure  the following  function should not throw exception.

public ErrorValue Init(ITcHmiSrvExtHost host, Context context)

If you are using log4net.config in the Init finction, make sure the following path is correct. 

“C:\\TwinCAT\\Functions\\TE2000-HMI-Engineering\\Bin\\x86\\TcHmiEngineeringExtensions.Server\\log4net.config”

Tips 06: We can have error sometimes that indicate that config file version mismatches, in this case we can’t publish the extension. See the following screenshot, where our module name is ServerExtensionCSharpOne and the config file is ServerExtensionCSharp:Config. The solution is we can change the version number in the config file and try again.

There is another way to solve this problem as well. 

=> Go to  HMI Server’s  config page

=> Press on TcHmiSrv tab in the configuration page

=> Press on Extensions, then the extension in questions and delete (upper right corner cross)

=> Publish it again, boom!

Tips 07:  Beckhoff publishes some of the modules via Nuget package management as compared to the previous releases ( for example SqliteHistorize and Event-related), we can end up with a situation that may be we don’t find the right version. One solution to this problem is to try the previous version of the package. The case below can be solved by installing the previous version (12.742.5) but 12.744.4 does not work.

How to take logs while we develop Extension

Sufficient logging allows answering nearly any question about what a program is doing. There are two sides we see to increased visibility: the first is visibility into what our code is doing after it’s running and people are using the software we have written. The second is visibility into your code during development. There are different ways to take log depending on framework we are talking about. In this page we shall describe how we can take logs for the Beckhoff HMI extension module.

TcHmiSrv is name space where we have Context. ITcHmiSrvExtHost interface has a method send. This can be used to write log file to the HomeAutomationMainDoorStatusReader\.engineering_servers\HomeAutomationMainDoorStatusReader as SQlite database. HomeAutomationMainDoorStatusReader is our project name.

 All extension developed with C#  is derived from the interface IExtension. When the server starts then it initialize the extension by calling the following method on the interface. We save the host  and context to use for the extension and creating log.
 
namespace TcHmiSrv.Management
{
    public interface IExtension
    {
        ErrorValue Init(ITcHmiSrvExtHost host, Context context);
    }
}

We need to hold the host and context and call send method to record in SQLite database.

         // Sends a message with the specified context, name, parameters and severity.
        public void Send(Context context, string name, IEnumerable<string> parameters = null, Severity severity = Severity.Severity_Info)
        {
            Message message = new Message(context, severity, name);

            if (parameters != null)
            {
                foreach (string parameter in parameters)
                {
                    message.Parameters.insert(message.Parameters.Size.ToString(), parameter);
                }
            }

            _host.send(context, message);
        }

This is an example when the init method is called we can record an appropriate message to the database.

_logger.Send(context, "MESSAGE_INIT Called OK");

The logger database file can be opened by the SQLite browser. Here are the screenshots for our home automation system step by step.

Finally we browse the database and see that our text is there.

We can use log4Net as well that we have described in this page https://www.hemelix.com/log4net/