
Variable mapping and Subscription Using JavaScript
You are designing an HMI where you want to interact with machines or its component in two ways. We want to give the command to the machine/plant and want to hear from the machine/plant, right? Each plant consists of thousands of different types of sensors, actuators, etc. Many of them can be similar and many of them can be dissimilar.
We need to link the PLC variables from the Visual studio projects. Let’s take an example, we have a water treatment plant where we have 50 similar pumps and 50 similar valves among other hundreds of similar devices. If we examine the following valve we can easily discover few variables, output signal from PLC, two proximity sensors (open/close), then we might have an alarm (we try to open but there is no air supply for example). If we need to link all variables one by one then it can difficult, confusing, time-consuming, etc. If this is a problem then you can continue reading this article.
Another practical problem we might have thought of in our compound user control case. We have an upper gate and a lower gate. Each of the gates has been called by using 18 parameters (https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-compound-user-control/). In a real project, we can have thousands of parameters that we need to pass. One issue is clear in this case:
-Name of the gate (‘Gate_LowerOne’)
-PLC variable path of the gate (PLC1.MAIN.ArrayGate.0)
If we pass only these 2 parameters then we should be able to derive the rest from the path of the variable
As we see in the following picture, we are passing 18 parameters, if we have more devices then we can have more variables, easily it can explode in our head!
In the following figure, we are configuring only the lower gate and we have so many variables to configure. Now compare with Figure 2 for the same result. For sure figure 2 is better than figure 1.

Figure 01: Parameters passing problems for complex user controls

Figure 02: Parameters passing simplified (compare figure 1 and 2)

Figure 03: A typical gate valve with automation components
We have a water inlet valve in water Plant A, the valve has six variables to monitor. Each time if we bind variables and want to get notifications. If we want to bind all these one by one could be a nightmare. One way to bind this variable inside a JavaScript function and call it from suitable places. If we pass PLC1.MAIN.waterPlantA.waterInletValve
for example, then we can subscribe to all these and notify as necessary. This idea we are going to implement in this exercise.
'PLC1.MAIN.waterPlantA.waterInletValve.isClosed'
'PLC1.MAIN.waterPlantA.waterInletValve.closeAfter'
'PLC1.MAIN.waterPlantA.waterInletValve.openProximitySensor'
'PLC1.MAIN.waterPlantA.waterInletValve.closeProximitySensor'
'PLC1.MAIN.waterPlantA.waterInletValve.alarm'
'PLC1.MAIN.waterPlantA.waterInletValve.warning'
Since we have our great home automation project, we go there back and implement some of the features like an exercise. I hope someday, this will be ready. We have picked the YardDoor. You drive by your car and you want to open the gate for you. Since it is a fictitious project we are far away from reality. But it still gives still very good experience about TwinCAT HMI.
Here are the rough functionalities:
There is a motor whose direction can be reversed by the contactor. When the forward contactor is active then it will open the gate but when both are active then it will close the gate. If we click on the Yard gate then it will show a popup with an open gate option. There is a proximity sensor that indicates that the gate is closed or not.
If the user press on the Open gate button then the motor starts and the gate opens. It takes 10 seconds to open the gate. The gate image moves to the left. After opening, it will keep open for 20 seconds and automatically closes the gate. Again the gate close takes 10 seconds. When the gate is at its home position then the proximity sensor indicates that it is in the closed position (home position).
Yard Gate, home position

Figure 04: The yard gate is in home position, sensor is active
Yard Gate, open position

Figure 05: The yard gate is in open position, sensor is not active
//Heaser of the program, please note also action
FUNCTION_BLOCK FN_MAINDOORYARD
VAR_INPUT
END_VAR
VAR_OUTPUT
isClosed : BOOL;
END_VAR
VAR
obstackcleDetected AT %I* : BOOL := FALSE; //No effect when opened or Closed, only when moving (closing or opening)
closeAfter : INT := 20; // Close the door after 20 seconds since opening the door, 10 Seconds to open and 10 seconds to close
closeProximitySwitch AT %I* : BOOL := FALSE;
openYardDoorOutput AT %Q* : BOOL := FALSE;
openYardDoorReverseOutput AT %Q* : BOOL := FALSE;
currentState : INT := yardDoorState.DOORCLOSED;
StateDescription : WSTRING := "Closed";
openCloseTimer : TON ;
keptOpenTimer : TON ;
openCommand : BOOL ;
closeCommand : BOOL ;
timeElapsed : INT := 0;
END_VAR
//Body of the program
processYardDoor();
CASE currentState OF
yardDoorState.DOORCLOSED:
isClosed:= TRUE;
openYardDoorReverseOutput:= FALSE;
openYardDoorOutput := FALSE;
closeProximitySwitch := TRUE;
openCloseTimer(IN:= FALSE);
keptOpenTimer(IN:= FALSE);
StateDescription := "Door Closed";
IF openYardDoorOutput AND NOT openYardDoorReverseOutput THEN
currentState := DOOROPENING;
END_IF
DOOROPENING:
openCommand := FALSE;
closeCommand := FALSE;
isClosed:= FALSE;
openYardDoorOutput := TRUE;
openCloseTimer(IN := (currentState = DOOROPENING ), PT := T#10S);
StateDescription := "Door Opening";
timeElapsed:= TIME_TO_INT(openCloseTimer.ET);
IF obstackcleDetected THEN
currentState:= DOORSTACKED;
END_IF
IF openCloseTimer.Q THEN
currentState:= DOOROPENED;
openCloseTimer(IN :=FALSE);
END_IF
DOORSTACKED:
isClosed:= FALSE;
StateDescription := "Door Stacked";
openYardDoorReverseOutput:= FALSE;
openYardDoorOutput := FALSE;
IF NOT obstackcleDetected THEN
currentState:= DOORCLOSING;
openCloseTimer(IN :=FALSE);
END_IF
DOOROPENED:
isClosed:= FALSE;
StateDescription := "Door Opened";
openYardDoorReverseOutput:= FALSE;
openYardDoorOutput := FALSE;
openCloseTimer(IN := (currentState = DOOROPENED), PT := INT_TO_TIME(1000 * closeAfter));
timeElapsed:= TIME_TO_INT(openCloseTimer.ET);
IF openCloseTimer.Q THEN
currentState:= DOORCLOSING;
openCloseTimer(IN :=FALSE);
END_IF
DOORCLOSING:
openCommand := FALSE;
closeCommand := FALSE;
isClosed:= FALSE;
StateDescription := "Door Closing";
openCloseTimer(IN := (currentState = DOORCLOSING), PT := T#10S);
timeElapsed:= TIME_TO_INT(openCloseTimer.ET);
openYardDoorReverseOutput:= TRUE;
openYardDoorOutput := TRUE;
IF obstackcleDetected THEN
currentState:= DOORSTACKED;
END_IF
IF openCloseTimer.Q THEN
currentState:= DOORCLOSED;
END_IF
DOORERROR:
StateDescription := "Door Error";
ELSE
StateDescription := "Door Error";
END_CASE;
IF closeProximitySwitch AND NOT obstackcleDetected THEN
currentState := yardDoorState.DOORCLOSED;
isClosed := TRUE;
END_IF
JavaScript for manipulating variables and control
// Keep these lines for a best effort IntelliSense of Visual Studio 2017 and higher.
/// <reference path="../../Packages/Beckhoff.TwinCAT.HMI.Framework.12.742.0/runtimes/native1.12-tchmi/TcHmi.d.ts" />
(function (TcHmi) {
var Functions;
(function (Functions) {
var SubscriptionLinking_Hemelix;
(function (SubscriptionLinking_Hemelix) {
function YardDoor(DeviceName, ControlName) {
if (TcHmi.Server.isWebsocketReady()) {
console.log(`DeviceName = ${DeviceName}`);
console.log(`ControlName = ${ControlName}`);
var myControl = TcHmi.Controls.get(ControlName);
if (myControl) {
var proximityElipse = TcHmi.Controls.get(ControlName.concat('.TcHmi_Controls_Beckhoff_TcHmiEllipse_DoorProximity'));
var imagefield = TcHmi.Controls.get(ControlName.concat('.TcHmi_Controls_Beckhoff_TcHmiImage_Door'));
var motorOnElipse = TcHmi.Controls.get(ControlName.concat('.TcHmi_Controls_Beckhoff_TcHmiEllipse_MotorON'));
var motorBackElipse = TcHmi.Controls.get(ControlName.concat('.TcHmi_Controls_Beckhoff_TcHmiEllipse_MotorBack'));
var timeElapse = TcHmi.Controls.get(ControlName.concat('.TcHmi_Controls_Beckhoff_TcHmiTextblock_7'));
var currentState = 0;
var signalOFF = {
"color": "rgba(219, 216, 216, 1)",
};
var signalON = {
"color": "rgba(0, 255, 0, 1)",
};
var commands = [
{
'symbol': 'PLC1.MAIN.fnYardDoor.isClosed'
},
{
'symbol': 'PLC1.MAIN.fnYardDoor.obstackcleDetected'
},
{
'symbol': 'PLC1.MAIN.fnYardDoor.closeAfter'
},
{
'symbol': 'PLC1.MAIN.fnYardDoor.closeProximitySwitch'
},
{
'symbol': 'PLC1.MAIN.fnYardDoor.timeElapsed'
},
{
'symbol': 'PLC1.MAIN.fnYardDoor.currentState'
},
{
'symbol': 'PLC1.MAIN.fnYardDoor.openYardDoorOutput'
},
{
'symbol': 'PLC1.MAIN.fnYardDoor.openYardDoorReverseOutput'
}
];
TcHmi.Server.subscribe(commands, 500, function (data) {
if (data.error !== TcHmi.Errors.NONE) {
// Handle TcHmi.Server class level error here.
return;
}
var response = data.response;
if (!response || response.error !== undefined) {
// Handle TwinCAT HMI Server response level error here.
return;
}
var commands = response.commands;
if (commands === undefined) {
return;
}
for (var i = 0, ii = commands.length; i < ii; i++) {
var command = commands[i];
if (command === undefined) {
return;
}
if (command.error !== undefined) {
// Handle TwinCAT HMI Server command level error here.
return;
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.isClosed') == 0) {
if (command.readValue == true) {
imagefield.setSrc('Images/MainDoor.svg');
//imagefield.setOpacity(1);
}
console.log(`isClosed called = ${command.readValue}`);
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.obstackcleDetected') == 0) {
console.log(`obstackcleDetected called = ${command.readValue}`);
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.closeAfter') == 0) {
console.log(`closeAfter called = ${command.readValue}`);
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.closeProximitySwitch') == 0) {
if (command.readValue == true) {
proximityElipse.setFillColor(signalON);
}
else {
proximityElipse.setFillColor(signalOFF);
}
console.log(`closeProximitySwitch called = ${command.readValue}`);
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.timeElapsed') == 0) {
var intval = command.readValue / 1000;
timeElapse.setText(intval.toString());
switch (currentState) {
case 0: //closed
{
var t = [{
"transformType": "Translate",
"x": 0,
"xUnit": "px",
"y": 0,
"yUnit": "px",
"z": 0,
"zUnit": "px",
}];
imagefield.setTransform(t);
}
break;
case 1: //opening
{
var t = [{
"transformType": "Translate",
"x": -intval * 20,
"xUnit": "px",
"y": 0,
"yUnit": "px",
"z": 0,
"zUnit": "px",
}];
imagefield.setTransform(t);
}
break;
case 2: //opened
{
var t = [{
"transformType": "Translate",
"x": -200,
"xUnit": "px",
"y": 0,
"yUnit": "px",
"z": 0,
"zUnit": "px",
}];
imagefield.setTransform(t);
}
break;
case 3: //closing
{
var t = [{
"transformType": "Translate",
"x": 20 * intval - 200,
"xUnit": "px",
"y": 0,
"yUnit": "px",
"z": 0,
"zUnit": "px",
}];
imagefield.setTransform(t);
}
break;
default:
break;
}
//console.log(`timeElapsed called = ${command.readValue}`);
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.currentState') == 0) {
currentState = command.readValue;
if (currentState == 2) {
var t = [{
"transformType": "Translate",
"x": -200,
"xUnit": "px",
"y": 0,
"yUnit": "px",
"z": 0,
"zUnit": "px",
}];
imagefield.setTransform(t);
}
console.log(`currentState called = ${command.readValue}`);
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.openYardDoorOutput') === 0) {
console.log(`openYardDoorReverseOutput command.readValue = ${command.readValue}`);
if (command.readValue == true) {
console.log(' openYardDoorOutput ON');
motorOnElipse.setFillColor(signalON);
}
else {
console.log(' openYardDoorOutput OFF');
motorOnElipse.setFillColor(signalOFF);
}
console.log(`openYardDoorOutput called = ${command.readValue}`);
}
if (commands[i].symbol.localeCompare('PLC1.MAIN.fnYardDoor.openYardDoorReverseOutput') === 0) {
console.log(`openYardDoorReverseOutput command.readValue = ${command.readValue}`);
if (command.readValue == true) {
console.log(' openYardDoorReverseOutput ON');
motorBackElipse.setFillColor(signalON);
}
else {
console.log(' openYardDoorReverseOutput OFF');
motorBackElipse.setFillColor(signalOFF);
}
console.log(`openYardDoorReverseOutput called = ${command.readValue}`);
}
}
}); //subscribe
} //myControl
} //isWebsocketReady
}
SubscriptionLinking_Hemelix.YardDoor = YardDoor;
})(SubscriptionLinking_Hemelix = Functions.SubscriptionLinking_Hemelix || (Functions.SubscriptionLinking_Hemelix = {}));
Functions.registerFunctionEx('YardDoor', 'TcHmi.Functions.SubscriptionLinking_Hemelix', SubscriptionLinking_Hemelix.YardDoor);
})(Functions = TcHmi.Functions || (TcHmi.Functions = {}));
})(TcHmi);
WebSocket
The WebSocket.readyState read-only property returns the current state of the WebSocket connection.
Value | State | Description |
---|---|---|
0 |
CONNECTING |
Socket has been created. The connection is not yet open. |
1 |
OPEN |
The connection is open and ready to communicate. |
2 |
CLOSING |
The connection is in the process of closing. |
3 |
CLOSED |
The connection is closed or couldn’t be opened. |
When web is ready then we can do read write variable. When the socket is open, we are able to read data from the server.
function CheckWebSocket()
{
var readyState = TcHmi.Server.getWebsocketReadyState();
if(readyState != null)
{
console.log(`Web socket not null`);
readyState = TcHmi.Server.getWebsocketReadyState();
if(readyState === WebSocket.OPEN){
console.log(`Web socket ready`);
} else {
console.log(`Web socket NOT ready`);
}
} else {
console.log(`Web socket null`);
}
}
CheckWebSocket();
Tips
01. Say we want to write some value based on the parameters or we want to derive the variable name based on the parameters. How can we achieve it? One way could be to pass a string that contains the name of the device and add a sensor to the monitor. For example, in our yard gate case, we have 2 outputs and 1 input signal. Say input proximity sensor name is gateClosedDetector. If we pass PLC1.MAIN.YarfdGate and the variable could be PLC1.MAIN.YarfdGate.gateClosedDetector. So we can pass PLC1.MAIN.YarfdGate as a parameter and we can drive the map variable and do the subscription.
=>We can pass the string that can be bounded as function binding. In this way we can extract the name and we can use it. As we can see the text is visible in on the popup and we can resolve it in the console. If we want to manipulate any variable based on this parameter by pressing a button on the popup. Note in the image below, how we are passing ‘PLC1.MAIN.YardGate’ note the quotation mark and it has been pressed on the fx.

Figure 06: How to pass a symbol path as parameter
var formattedSymbol6 = '%pp%ExtraStringAdded%/pp%';
TcHmi.Symbol.readEx2(formattedSymbol6, function(data) {
if (data.error === TcHmi.Errors.NONE) {
var value = data.value;
console.log(`ExtraStringAdded read OK = ${value}`);
} else {
console.log(" ExtraStringAdded failed");
}
});
Download the modified code stringPassingtoPopup_hemelix.zip
How to read variable and initialized a control in Popup:
There are different ways to do this, we shall describe a method here. We have to initialize a control (image, Text, or whatever in the Popup by manipulating a PLC Path variable.)
We modify the sample, YardDoor control. When we create the Popup (whenever we press on the door icon, it will create a Popup). In that Popup, we introduced a textBox and we shall write the elapsed time (the value will be 1-time update). If we want to do it continuously we can use a subscription mechanism for example. As we see a textBox time is different for the popup.
Now TextControl id is TcHmi_Controls_Beckhoff_TcHmiTextblock_8 (though we should have meaningful name).

Figure 07: Relation between control id and the value in popup case
We introduce new Popup variable Device path as shown in the following image.

Figure 08: Variable path has been passed as DevicePath a string type
We are now passing the path as a string ‘PLC1.MAIN.fnYardDoor’ as shown in the following figure. There are few variable in the function block such as isClosed, obstacleDetected, timeElapsed etc. We just pass a single string and from there we create the full variable and read the value to manipulate or display.

Figure 09: Way to pass symbol path (also called variable path)
The final embedded code is show in the following image. This whole project can be downloaded as well. First we read the parameter DevicePath which should be ‘PLC1.MAIN.fnYardDoor’. Now we can add .timeElapsed and read the server variable and initialized to the edit controls. If we move the yard gate then the value will be changed and and each time the text Box will show different value.

Figure 10: Building actual variable name from the parameter and reading it
Youtube video
Download the modified program SubscriptionLinking_Hemelix_v2.zip , if you want to support us anyway, you could contact us.
Advanced topics about Symbol, Symbol expression and Binding
We have described symbols, compound user control, and how to reduce the number of parameters passing for user control has been described. You need some knowledge about user controls, JavaScript, and subscription mechanisms in TwinCAT.
Here is the link to how you should go through the tutorial.
=> Symbol, Symbol Expression, and bindins this page https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-symbol/
=> Compound user control, https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-compound-user-control/
=> How to reduce the number of parameters when using compound user control, https://www.hemelix.com/scada-hmi/twincat-hmi/mapping-and-subscription-using-js/
You can watch the video, and go through the source code. If you have an issue then you can comment on the YouTube video or in our Google group
Tips
When we subscribe a variable then it must be exact to the variable name and also the mapped path. If there is mismatch then we get an error code 513 as shown in the following figure.

Figure 11: Subscription can fail due to various reason
References:
Know more about JSON, see https://json-schema.org/understanding-json-schema/basics.html
Difference among var let https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/
https://www.javascripttutorial.net/
https://javascript.info/promise-basics
https://jsfiddle.net/ (Online JavaScript Test)
https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/JavaScript_basics
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
https://www.w3schools.com/js/default.asp
https://www.w3schools.com/js/js_graphics_chartjs.asp (Drawings by JavaScript)
https://tools.knowledgewalls.com/json-to-string JSON String to String
http://www.jsondiff.com/ (Compare JS file)
https://infosys.beckhoff.com/english.php?content=../content/1033/te2000_tc3_hmi_engineering/3758305291.html&id= (HMI error code)
JavaScript tutorial: https://masteringjs.io/fundamentals
Download the sample from the link given above.
See next how to change device settings at https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-device-setting-and-popup/
Ask questions related to Hemelix sample code and design at Google group https://groups.google.com/g/hemelix