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

Figure 1: TwinCAT extension architecture

Relation between TwinCAT HMI client and custom module

Figure 2: TwinCAT client and HMI server communication

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.

Figure 3: C# Extension Module is our target in this tutorial

Steps for building extension in C#

The module we are developing will read data from the SQLite database and will be displayed on HMI. If we see figure 3 then we can see a clear picture of what are we going to do in this tutorial. We develop the C# Extension Module. Our focus is the extension, not HMI, not SQLite database. We have separate tutorials for those.

STEP 01: In Visual Studio, select,  Add a new project, select the CSharp extension then Next and Create

Figure 4: TwinCAT  C# extension creation by the wizards provided in HMI Engineering

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

Figure 5: TwinCAT  C# extension creation, select the right version if you multiple HMI engineering installed in your system

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

Figure 6: TwinCAT  C# extension creation, both projects (HMI and Extension in the same solutions)

Now we should be able to build the project successfully.  Browse the generated code and you can find the OnRequest method we have a switch case for mapping different commands from HMI. We replace ” YOUR_MAPPING” with a suitable command string in the following snippet.

switch (command.Mapping)
{
    // case "YOUR_MAPPING":
    //     Handle command
    //     break;
    default:
    command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Fail;
    command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
    break;
}

A different version of the wizards creates a different type of code. The version we are using here is 1.12.752.0. In the previous version, it was generating a bit different code but the idea is the same, so be aware of that.

At this phase, if we want to compile the project that should be compiled perfectly though it does not do much!

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 (xx is your project name).  We can edit the symbols. The wizards have created the following JSON code. We add our command inside the symbols.

{
  "$schema": "ExtensionSettings.Schema.json",
  "guid": "8e25d4f4-d586-4006-845b-a6b9fc1c4579",
  "version": "1.0.0.0",
  "configVersion": "1.0.0.0",
  "policies": [
    "StrictPropertyValidation"
  ],
  "symbols": {}
}

 

We shall use the following commands’ names inside the symbols. We are using the same sample which we have described on the page https://www.hemelix.com/scada-hmi/twincat-hmi/data-grid-view/,  see figure 3 to understand what we are talking about.

    "GetDataValue": {
      "readValue": {
        "$ref": "tchmi:general#/definitions/String"
      },
      "hidden": false
    },
    "WriteValueInit": {
      "readValue": {
        "$ref": "tchmi:general#/definitions/String"
      },
      "hidden": false
    },
    "WriteValueDelete": {
      "readValue": {
        "$ref": "tchmi:general#/definitions/String"
      },
      "hidden": false
    },
    "WriteValueAdd": {
      "readValue": {
        "$ref": "tchmi:general#/definitions/String"
      },
      "hidden": false
    }

STEP 03: If the project was compiled successfully, the extension can be loaded from the server. So we need a reference for the extension from the HMI project.

HMI Project References | Add References | Select the extension.

STEP 04: 

Let’s fill the switch case with our symbols which we added in the previous steps. We shall pass these strings from our JavaScript call from the HMI. And it depends on our requirements and what we write in these switch cases.

 private void OnRequest(object sender, TcHmiSrv.Core.Listeners.RequestListenerEventArgs.OnRequestEventArgs e)
        {
            try
            {
                e.Commands.Result = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
                foreach (Command command in e.Commands)
                {
                    try
                    {
                        // Use the mapping to check which command is requested
                        switch (command.Mapping)
                        {
                            case "WriteValueInit":
                                break;
                            case "WriteValueDelete":
                                break;
                            case "WriteValueAdd":
                                break;
                            case "GetDataValue":
                                break;
                            default:
                                break;
                        }
                    }
                    catch (Exception ex)
                    {
                    }
                }
            }
            catch (Exception ex)
            {
                throw new TcHmiException(ex.ToString(), ErrorValue.HMI_E_EXTENSION);
            }
        }

STEP 05:

Bring the HMI Configuration menu as shown in the following figure for mapping those variables.

 

Figure 7: The symbols for extension can be mapped as an ADS variable.

 

We must return the status of the operation in each case,  in Version: 1.12.752.0 is a bit different than the older version but the concept is similar. If the operation result is OK then we return 0x0 (success) or 0x1 (Fail) For example, we don’t expect to have a default case and we return fail.

case "1" :
{}
break;
 default:
command.ExtensionResult = ServerExtensionReadWriteErrorValue.ServerExtensionReadWriteFail;
command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";

Figure 8: Extension variable can be mapped just like ADS variables.

The symbol can be mapped as we are mapping variables from a PLC. The symbol can be used from the Action and condition editor or can be used by JavaScript. This extension can do any work as the C# application in windows can do. Without this, we may not be able to implement many cases only in HTML/JavaScript/CSS. There are two ways by using the symbol binding mechanism that we have used already in the different projects already. In figure 8, we are using the following code for getting data. The data is formatted as needed by the Grid View in extension, so we don’t need to do anything in HMI.

%s%ServerExtensionCSharpEmpty1.GetDataValue%/s%

Figure 9: Data source for the Grid view is the extension

We can add a data item to the extension by the following code, we are using the command WriteValueAdd: In this case, the symbol expression is 

'%s%ServerExtensionCSharpEmpty1.WriteValueAdd%/s%'

The data from the HMI is collected as a JavaScript object (uiData). The object is converted to a JSON string and then pass to the writeEx function. This is an asynchronous function that will be called back with data as a parameter that will tell the status of the call. If we need to call a few dependent asynchronous calls then it becomes difficult to follow. We can convert the asynchronous call to a synchronous call as described on page https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-async-function/ see section Asynchronous call as synchronous.

var myJSONString = JSON.stringify(uiData);
TcHmi.Symbol.writeEx('%s%ServerExtensionCSharpEmpty1.WriteValueAdd%/s%', myJSONString, function (data) {
if (data.error === TcHmi.Errors.NONE) {
     console.log('One item writing to extension OK');
} else {
     console.log('Write was not OK data.details.code = ${data.details.code}');
}

The following code is the implementation of the OnRequest method. Whenever we use this extension module then the call comes here. The TwinCAT framework forwards the call for us. We don’t need to do any special.

// Called when a client requests a symbol from the domain of the TwinCAT HMI server extension.
private void OnRequest(object sender, TcHmiSrv.Core.Listeners.RequestListenerEventArgs.OnRequestEventArgs e) {
  try {
    e.Commands.Result = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
    foreach(Command command in e.Commands) {
      try {
        // Use the mapping to check which command is requested
        switch (command.Mapping) {
        case "WriteValueInit":
          {
            lock(myLock) {
              if (!initDone) {
                sqlConnector = new SQLConnecter();
                sqlConnector.Do();
                initDone = true;
              }
            }
          }
          command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
          command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
          break;
        case "WriteValueDelete":
          {
            if (command.IsSet == true && command.WriteValue != null) {
              int index = Int32.Parse(command.WriteValue);
              sqlConnector.DeleteLine(index);
            }
          }
          command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
          command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
          break;
        case "WriteValueAdd":
          if (command.IsSet == true && command.WriteValue != null) {
            Data routes_list = JsonConvert.DeserializeObject < Data > (command.WriteValue);
            sqlConnector.AddItem(routes_list.Test1, routes_list.Test2, routes_list.Test3, routes_list.ComboBox);
          }
          command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
          command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
          break;
        case "GetDataValue":
          if (SQLConnecter.MyData.Length <= 0) {
            command.ReadValue = "{}";
          }
          else {
            command.ReadValue = SQLConnecter.MyData;
          }
          command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
          command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
          break;
        default:
          command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Fail;
          command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
          break;
        }
      }
      catch(Exception ex) {
        command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Fail;
        command.ResultString = "Calling command '" + command.Mapping + "' failed! Additional information: " + ex.ToString();
      }
    }
  }
  catch(Exception ex) {
    throw new TcHmiException(ex.ToString(), ErrorValue.HMI_E_EXTENSION);
  }
}

Symbol access right from the server extension is as follows:

Figure 10: Access right definitions

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. 


}, "ServerExtensionCSharpEmpty1.WriteValueAdd": { "ACCESS": 3, "DOMAIN": "ServerExtensionCSharpEmpty1", "HIDDEN": false, "MAPPING": "WriteValueAdd", "OPTIONS": {}, "SCHEMA": { "$ref": "tchmi:general#/definitions/String" }, "USEMAPPING": true }, "ServerExtensionCSharpEmpty1.WriteValueDelete": { "ACCESS": 3, "DOMAIN": "ServerExtensionCSharpEmpty1", "HIDDEN": false, "MAPPING": "WriteValueDelete", "OPTIONS": {}, "SCHEMA": { "$ref": "tchmi:general#/definitions/String" }, "USEMAPPING": true }, "ServerExtensionCSharpEmpty1.WriteValueInit": { "ACCESS": 3, "DOMAIN": "ServerExtensionCSharpEmpty1", "HIDDEN": false, "MAPPING": "WriteValueInit", "OPTIONS": {}, "SCHEMA": { "$ref": "tchmi:general#/definitions/String" }, "USEMAPPING": true },

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

The server extension must  be activated. If the extension is not activated then it’s activation symbol is red and in activated state it is green, see figure 11.

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. Users can press the 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.

Figure 11: Final sample application, the status is defined in the extension and that is  visible in the HMI

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

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

How to debug extension:

=> Way 1:

See the tips section, How to take logs while we develop Extension

=> Way 2: https://www.hemelix.com/software/log4net-for-hmi-extension/

Tips:

Tips 00a:  

When the value of IsSet is changed, whenever the command has been updated either the ReadValue or the WriteValue of the command. If we write something to the server extension variable then there will be value in the WriteValue field. The value will stay for one cycle.

public bool IsSet { get; }
public Value WriteValue { get; set; }
public Value ReadValue { get; set; }
if (command.IsSet == true && command.WriteValue != null)
{  
//Command has been set and there is something that has been written to command.WriteValue
//For example by WriteToSymbol or JavaScript in the action window
}
else
{
//Other branch
}
case "ReadValueString":  // for reading variable from the server (see the full code at the end of this page)
if (command.IsSet == true) // IsSet is  false
{
command.ReadValue = 7777;
}
command.ReadValue = "something";
if (command.IsSet == true)  // now it is true because of the previous action
{
}

 

 

Tips 00b:  If you are creating an extension in version Product Version: 1.12.742.5, Version: 1.12.742.5 (not sure about minor version) or later, you need to include the extension version project as reference to the HMI project. you can create a package by the extension and add it to the project as references, then we shall get a chance to activate the extension and you can used the mapped variables as described before.

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)

Figure 13: Locations of module in the HMI Server

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.

Figure 14: Mismatches of extension provided by Beckhoff. This must be compatible with our Server version.

Tips 08:  License is needed when we activate the extension. We need to purchase a license for HMI Server.

=> Add a reference of the extension DLL or project to the HMI project

=> Add TC3 HMI Extension SDK license plus other licenses if needed

=> Activate the 7 days trial license

=> We can create a small PLC project and we select the HMI Server as target and activate the license

=> Following figure shows that ExtensionTest has been added to reference (Reference | Add Reference)

=> It should be visible as green (if activated) and as red (if not activated)

=> You can click also on the HMI server config page to activate the extension

Figure 16: Extension in activated state

Code as it was mentioned in the Tips 00a

{
  "$schema": "ExtensionSettings.Schema.json",
  "guid": "f0c3bd99-c625-428e-b831-fcf76a666c3b",
  "version": "1.0.0.0",
  "configVersion": "1.0.0.6",
  "policies": [
    "StrictPropertyValidation"
  ],
  "symbols": {
    "ReadValueString": {
      "readValue": {
        "allOf": [
          {
            "$ref": "tchmi:general#/definitions/String"
          },
          {
            "readOnly": true
          }
        ]
      },
      "hidden": false
    },
    "WriteValueInt": {
      "readValue": {
        "$ref": "tchmi:general#/definitions/Integer"
      },
      "hidden": false
    }
  }
}
switch (command.Mapping)
{
case "ReadValueString":
//At this point IsSet is false
command.ReadValue = 7777;
//At this point IsSet is true
if (command.IsSet == true)
{
command.ReadValue = 7777;  //This will be executed
}
command.ExtensionResult = ServerExtensionReadWriteErrorValue.ServerExtensionReadWriteSuccess;
command.ResultString = "ReadValue1 '" + command.Mapping + "' not handled.";
break;
case "WriteValueInt":
command.ReadValue = aString;
if (command.IsSet == true && command.WriteValue != null)
{
command.ReadValue = 7777; // If UI has written something to WriteValueInt will have value for current cycle
}
else
{
}
command.ExtensionResult = ServerExtensionReadWriteErrorValue.ServerExtensionReadWriteSuccess;
command.ResultString = "WriteValue1 '" + command.Mapping + "' not handled.";
break;

Tips 09 => We see the following message when the extension is not activated, we activate the extension and try again

TcHmiFramework.js:687 [2022-04-17T07:39:48.523Z][Error] [Source=Framework, Module=TcHmi.System.TriggerManager, Event=Desktop.onAttached, ObjectType=WriteToSymbol] Code: 3005/0xbbd, Message: E_SERVER_COMMAND_ERROR
  Reason: %s%ServerExtensionCSharpEmpty1.WriteValueInit%/s%: Error in command for symbol: 'ServerExtensionCSharpEmpty1.WriteValueInit' in response from server with id: '7'.
  Domain: TcHmi.System.Symbol
  as result of: Code: 2050/0x802, Message: INVALID_DOMAIN
    Reason: Invalid domain
    Domain: TcHmiSrv

How to take logs while we develop Extension

Using SQLite ( Some HMI engineering version it is built in and some don't have)

For taking a log by using log4net see the following:

https://www.hemelix.com/software/log4net-for-hmi-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 logs depending on the framework we are talking about. On this page, we shall describe how we can take logs for the Beckhoff HMI extension module.

 

TcHmiSrv is the namespace 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 initializes the extension by calling the following method on the interface. We save the host and context to use for the extension and creating a 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 on this page https://www.hemelix.com/software/log4net-for-hmi-extension/

https://infosys.beckhoff.com/index.php?content=../content/1031/te2000_tc3_hmi_engineering/9007203183297675.html&id=

Download the sample from the link given above.

Next, let’s try to understand how to display SQL data to HMI at https://www.hemelix.com/sql/sql-data-modeling/

Ask questions related to Hemelix sample code and design at Google group https://groups.google.com/g/hemelix