PLC Setup and Requirements
The code for the PLC project is here. According to the Beckhoff documentation for their JSON functions, TwinCAT 3 build 4022 on an x86, x64 or ARM processor can support JSON development. For this post, we will be using TwinCAT 3 build 4024.32 with a VM running windows 10.
For our outbound JSON will want to create a JSON structure that displays the available commands, the status, and a section of meta-data common to all instances. Our inbound JSON will just contain the active command that has been requested by the external control source (HMI, other PLC, etc.).
The SAX and the DOM function blocks are both capable of reading and writing the JSON object but come with their own drawbacks. The SAX function blocks can be thought of as line-by-line where each line is processed in sequential order. The DOM function blocks store the JSON into the IPC memory and access the whole JSON at once for reading and writing. While the SAX writer is easier to use and can be a tad more efficient, it requires parameterizing arrays to contain all the data for the JSON. If your JSON structure is relatively static and the length is similar across all devices, then this may be the best choice for you. The DOM parser allows for more flexibility and dynamic JSON creation at the cost of complexity.
TYPE ST_JsonInterface_Measurement :
STRUCT
sName : STRING;
pValue : PVOID;
eValueType : __SYSTEM.TYPE_CLASS;
nValueSize : DINT;
END_STRUCT
END_TYPE
TYPE ST_JsonInterface_Command :
STRUCT
Name : STRING;
Parameters : ARRAY[0..15] OF ST_JsonInterface_CommandParameter;
END_STRUCT
END_TYPE
TYPE ST_JsonInterface_CommandParameter :
STRUCT
sName : STRING;
eParamType : E_CommandParameterTypes;
END_STRUCT
END_TYPE
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE E_CommandParameterTypes :
(
NullType := 0,
BoolType := 1,
NumericType := 2,
StringType := 3
);
END_TYPE
FUNCTION_BLOCK FB_SaxJson
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
(* Outbound Json *)
sDeviceName : STRING;
fbJsonSaxWriter : FB_JsonSaxWriter; //Outbound Json SAX type Json Writer
astOutboundMeasurements : ARRAY[0..15] OF ST_JsonInterface_Measurement;
astOutboundCommands : ARRAY[0..15] OF ST_JsonInterface_Command;
sOutboundJson : STRING(2000);
END_VAR
Now that we have the basic DUTs setup, we can start by starting the JSON document and building the data objects. To start the JSON document, call fbJsonSaxWriter.StartObject() at the beginning of the POU and call fbJsonSaxWriter.EndObject() at the end. The code for the JSON values will be between these two method calls. Create 3 methods called SetupOutboundMetaData, SetupOutboundCommands, and SetupOutboundStatus and call these methods between the StartObject and EndObject calls we just created. We will add want to add a fbJsonSaxWriter.CopyDocument method call to the outbound json string at the end of the object. Using the copy document is important as the GetDocument method call is limited to a 255 character string. To ensure the document is fresh every scan, we will finish the outbound method calls with a fbJsonSaxWriter.ResetDocument.
(* Outbound *)
fbJsonSaxWriter.StartObject(); // Start the json object
SetupOutboundMetaData(); // Add meta_data object to the json
SetupOutboundCommands(); // Add commands object to the json
SetupOutboundStatus(); // Add status object to the json
fbJsonSaxWriter.EndObject(); // End the json object
(* Copy the json to the large string *)
fbJsonSaxWriter.CopyDocument(sOutboundJson, SIZEOF(sOutboundJson));
(* Reset the outbound json document *)
fbJsonSaxWriter.ResetDocument();
Starting with SetupOutboundMetaData we will add the ‘meta_data’ key and create an object to contain the meta_data information. In this method we will also add the name, timestamp, json size, and the JSON length by using the Key-value pair assignments within FB_JsonSaxWriter.
METHOD PRIVATE SetupOutboundMetaData
(* Create the status meta-data object *)
fbJsonSaxWriter.AddKey('meta_data');
fbJsonSaxWriter.StartObject();
(* Add Name *)
fbJsonSaxWriter.AddKey('name');
fbJsonSaxWriter.AddString(sDeviceName);
(* Add Timestamp *)
fbJsonSaxWriter.AddKey('timestamp');
fbJsonSaxWriter.AddUlint(F_GetSystemTime());
(* Add 'json_size' to the meta-data object *)
fbJsonSaxWriter.AddKey('json_size');
fbJsonSaxWriter.AddUdint(fbJsonSaxWriter.GetDocumentLength());
(* Add 'json_length' to the meta-data object *)
fbJsonSaxWriter.AddKey('json_length');
fbJsonSaxWriter.AddUdint(LEN2(ADR(sOutboundJson)));
(* End the 'outbound_meta_data' object *)
fbJsonSaxWriter.EndObject();
Now that we have added the metadata to the JSON structure, we will add commands to the JSON. For this we will create a private method call HelperCreateCommandObject and two public methods called AddCommand and AddCommandParameter. The helper method will make it easier to add the command to the JSON while the public methods add the commands to the function block arrays. To add the commands to the FB, the public methods will simply insert the command name into the array and add the specified parameter to that command in the array.
To add the command to the JSON, we will iterate through the command array and add each command using the helper. Because we want to check if the command has any parameters, we will need to have an empty copy of a command and compare the parameters structures. If the parameter structure of the empty command matches that of the current iteration, then no parameters are present. Notice that we create the commands object, iterate through using the helper, then close the object all within the same method call.
METHOD AddCommand
VAR_INPUT
iInIndex : UDINT := 0;
sInName : STRING;
END_VAR
IF iInIndex > 15 THEN
// Cannot add command if index is out of bounds
// Option to add error handling
RETURN;
END_IF
IF sInName = '' OR F_ToLCase(sInName) = 'null' THEN
// Ignore if the command is blank or null
// Option to add error handling
RETURN;
END_IF
// Add the command to the array
astOutboundCommands[iInIndex].Name := sInName;
METHOD AddCommandParameter
VAR_INPUT
iInIndex : UDINT := 0;
sInCommandName : STRING;
sInParameterName : STRING;
eInParameterType : E_CommandParameterTypes;
END_VAR
VAR
i : UDINT;
END_VAR
IF iInIndex > 15 THEN
// Cannot add command if index is out of bounds
// Option to add error handling
RETURN;
END_IF
IF sInCommandName = '' OR F_ToLCase(sInCommandName) = 'null' THEN
// Ignore if the command is blank or null
// Option to add error handling
RETURN;
END_IF
IF sInParameterName = '' OR F_ToLCase(sInParameterName) = 'null' THEN
// Ignore if the parameter is blank or null
// Option to add error handling
RETURN;
END_IF
// Iterate through the commands and locate the command with a specific name.
// Add the parameter name and type to the located command.
FOR i := 0 TO 15 DO
IF astOutboundCommands[i].Name = sInCommandName THEN
astOutboundCommands[i].Parameters[iInIndex].sName := sInParameterName;
astOutboundCommands[i].Parameters[iInIndex].eParamType := eInParameterType;
RETURN;
END_IF
END_FOR
METHOD PRIVATE SetupOutboundCommands
VAR_INPUT
END_VAR
VAR
i : UDINT;
stEmptyCommand : ST_JsonInterface_Command;
bHasParams : BOOL;
END_VAR
MEMSET(ADR(stEmptyCommand), 0, SIZEOF(stEmptyCommand));
bHasParams := FALSE;
(* Add 'commands' key and start the 'commands' object *)
fbJsonSaxWriter.AddKey('commands');
fbJsonSaxWriter.StartObject();
(* Iterate through the command array and add each command to the overarching 'commands' object *)
FOR i := 0 TO 15 DO
bHasParams := MEMCMP(ADR(stEmptyCommand.Parameters), ADR(astOutboundCommands[i].Parameters), SIZEOF(stEmptyCommand.Parameters)) 0;
Helper_CreateCommandObject(stInCommand := astOutboundCommands[i], bHasParams);
END_FOR
(* End the 'commands' object *)
fbJsonSaxWriter.EndObject();
METHOD PRIVATE Helper_CreateCommandObject
VAR_INPUT
stInCommand : ST_JsonInterface_Command;
bInHasParams : BOOL;
END_VAR
VAR
i : UDINT;
END_VAR
// Exit method if command name is empty or null
IF stInCommand.Name = '' OR F_ToLCase(stInCommand.Name) = 'null' THEN
RETURN;
END_IF
// Create command object
fbJsonSaxWriter.AddKey(stInCommand.Name);
fbJsonSaxWriter.StartObject();
// Add 'parameters' key to the command object
fbJsonSaxWriter.AddKey('parameters');
// Check if command has parameters
IF bInHasParams THEN
// Add parameter object if parameters are found
fbJsonSaxWriter.StartObject();
// Iterate through parameters and add them to the 'parameters' object
FOR i := 0 TO 15 DO
IF stInCommand.Parameters[i].sName = '' THEN
// skip since the parameter is not named
CONTINUE;
ELSIF F_ToLCase(stInCommand.Parameters[i].sName) = 'null' THEN
// skip since the parameter is not named or is null
CONTINUE;
ELSE
fbJsonSaxWriter.AddKey(stInCommand.Parameters[i].sName);
CASE stInCommand.Parameters[i].eParamType OF
E_CommandParameterTypes.NullType:
fbJsonSaxWriter.AddNull();
E_CommandParameterTypes.BoolType:
fbJsonSaxWriter.AddString('bool');
E_CommandParameterTypes.NumericType:
fbJsonSaxWriter.AddString('numeric');
E_CommandParameterTypes.StringType:
fbJsonSaxWriter.AddString('string');
END_CASE
END_IF
END_FOR
// End the 'parameters' object
fbJsonSaxWriter.EndObject();
ELSE
// Add null if no parameters are found
fbJsonSaxWriter.AddNull();
END_IF
// End the command object
fbJsonSaxWriter.EndObject();
Now that we have added the commands, we will add the status to the JSON. Within the status, we want to include the state, a Boolean faulted tag, and the measurements. Start by adding the ‘Status’ object to the JSON. To add the state and the faulted tags, simply use the AddKeyString and AddKeyBool methods of the SAX writer FB. After adding the basic ‘measurements’ object where we will iterate through the measurement array and add the value to the JSON. Since we used the ANY data type, we need to setup a series of pointers to change the data type from a PVOID to a readable value. After iteration, be sure to close the ‘measurements’ object and the ‘status’ object at the end of the method call.
METHOD SetupOutboundStatus
VAR_INPUT
END_VAR
VAR
i : UDINT;
stMeasurement : ST_JsonInterface_Measurement;
stPointers : ST_JsonInterface_TypePointers;
bTempBool : BOOL;
sTempString : STRING;
sMessage : STRING;
sState : STRING;
bFaulted : BOOL;
END_VAR
(* Add and start the 'Status' object *)
fbJsonSaxWriter.AddKey('status');
fbJsonSaxWriter.StartObject();
(* Add 'state' key and value *)
fbJsonSaxWriter.AddKeyString('state', sState);
(* Add 'faulted' key and value *)
fbJsonSaxWriter.AddKeyBool('faulted', bFaulted);
(* Add 'measurements' key and start the 'measurements' object *)
fbJsonSaxWriter.AddKey('measurements');
fbJsonSaxWriter.StartObject();
(* Iteratate through result array and add each 'measurement' to the 'measurements' object *)
FOR i := 0 TO 15 DO
stMeasurement := astOutboundMeasurements[i];
// Skip iterration method since no name is assigned
IF stMeasurement.sName = '' OR F_ToLCase(stMeasurement.sName) = 'null' THEN
CONTINUE;
END_IF
(* Add the result name *)
fbJsonSaxWriter.AddKey(stMeasurement.sName);
(* Add the result value *)
CASE stMeasurement.eValueType OF
__SYSTEM.TYPE_CLASS.TYPE_BOOL:
stPointers._pBOOL := stMeasurement.pValue;
fbJsonSaxWriter.AddBool(stPointers._pBOOL^);
__SYSTEM.TYPE_CLASS.TYPE_BYTE:
stPointers._pBYTE := stMeasurement.pValue;
fbJsonSaxWriter.AddBase64(
stPointers._pBYTE, stMeasurement.nValueSize
);
__SYSTEM.TYPE_CLASS.TYPE_WORD:
fbJsonSaxWriter.AddHexBinary(
stMeasurement.pValue, stMeasurement.nValueSize
);
__SYSTEM.TYPE_CLASS.TYPE_DWORD:
fbJsonSaxWriter.AddHexBinary(
stMeasurement.pValue, stMeasurement.nValueSize
);
__SYSTEM.TYPE_CLASS.TYPE_LWORD:
fbJsonSaxWriter.AddHexBinary(
stMeasurement.pValue, stMeasurement.nValueSize
);
__SYSTEM.TYPE_CLASS.TYPE_SINT:
stPointers._pSINT := stMeasurement.pValue;
fbJsonSaxWriter.AddDint(SINT_TO_DINT(stPointers._pSINT^));
__SYSTEM.TYPE_CLASS.TYPE_USINT:
stPointers._pUSINT := stMeasurement.pValue;
fbJsonSaxWriter.AddUdint(USINT_TO_UDINT(stPointers._pUSINT^));
__SYSTEM.TYPE_CLASS.TYPE_INT:
stPointers._pINT := stMeasurement.pValue;
fbJsonSaxWriter.AddDint(INT_TO_DINT(stPointers._pINT^));
__SYSTEM.TYPE_CLASS.TYPE_UINT:
stPointers._pUINT := stMeasurement.pValue;
fbJsonSaxWriter.AddUdint(UINT_TO_UDINT(stPointers._pUINT^));
__SYSTEM.TYPE_CLASS.TYPE_DINT:
stPointers._pDINT := stMeasurement.pValue;
fbJsonSaxWriter.AddDint(stPointers._pDINT^);
__SYSTEM.TYPE_CLASS.TYPE_UDINT:
stPointers._pUDINT := stMeasurement.pValue;
fbJsonSaxWriter.AddUdint(stPointers._pUDINT^);
__SYSTEM.TYPE_CLASS.TYPE_LINT:
stPointers._pLINT := stMeasurement.pValue;
fbJsonSaxWriter.AddLint(stPointers._pLINT^);
__SYSTEM.TYPE_CLASS.TYPE_ULINT:
stPointers._pULINT := stMeasurement.pValue;
fbJsonSaxWriter.AddUlint(stPointers._pULINT^);
__SYSTEM.TYPE_CLASS.TYPE_REAL:
stPointers._pREAL := stMeasurement.pValue;
fbJsonSaxWriter.AddReal(stPointers._pREAL^);
__SYSTEM.TYPE_CLASS.TYPE_LREAL:
stPointers._pLREAL := stMeasurement.pValue;
fbJsonSaxWriter.AddLreal(stPointers._pLREAL^);
__SYSTEM.TYPE_CLASS.TYPE_STRING:
stPointers._pSTRING := stMeasurement.pValue;
fbJsonSaxWriter.AddString(stPointers._pSTRING^);
__SYSTEM.TYPE_CLASS.TYPE_WSTRING:
stPointers._pWSTRING:= stMeasurement.pValue;
sTempString := WSTRING_TO_STRING(stPointers._pWSTRING^);
fbJsonSaxWriter.AddString(sTempString);
__SYSTEM.TYPE_CLASS.TYPE_TIME:
stPointers._pTIME := stMeasurement.pValue;
sTempString := TIME_TO_STRING(stPointers._pTIME^);
fbJsonSaxWriter.AddString(sTempString);
__SYSTEM.TYPE_CLASS.TYPE_DATE:
stPointers._pDATE := stMeasurement.pValue;
sTempString := DATE_TO_STRING(stPointers._pDATE^);
fbJsonSaxWriter.AddString(sTempString);
ELSE
fbJsonSaxWriter.AddNull();
END_CASE
END_FOR
(* End the 'measurement' object *)
fbJsonSaxWriter.EndObject();
(* Close the 'status' object *)
fbJsonSaxWriter.EndObject();
Parsing the JSON using the SAX Reader
I recommend against using the SAX reader in this use case as it parses the JSON line by line. If you are set on using the SAX reader, refer to the Beckhoff sample program here. The SAX reader requires you to create a handler and to exam each line to determine if the current line equals the requested command. Because we are setting up the commands to be dynamic and adjustable, using the SAX reader limits us to singular commands and increases in complexity as the JSON becomes nested. Using the DOM parser avoids this.
The SAX reader is very effective at parsing constant structured JSON strings and can be valuable to determine a JSONs validity.
Check back next month for the last installment in the JSON series. In the meantime, contact us if you need more information.