JS Part 1                                                                                                                                                                                  JS Part 2

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.

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

Yard Gate, open position

 

//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);


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.

There are two ways we can pass it:

=>We can make a localized string  and pass it as shown in the following image. Text3 can be assigned to PLC1.MAIN.YarfdGate, Note the symbol as marked, if it is not bindable then the color 

=>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.

 

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).

We introduce  new Popup variable Device path as shown in the following image. 

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.

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.

Download the  modified program SubscriptionLinking_Hemelix_v2.zip , if you want to support us anyway, you could contact us.