The simplest way to handle recipe messages with TransSECS is through the simple automatic file-based recipe manager. This stores an incoming recipe in text based files using the PPID as the file name, and storing the PPBody value in the file. This simple recipe management is not enabled by default. It is enabled by setting the TransSECS configuration property, transsecs.recipemanager=1 (see TransSECS - Configuration).
With this option enabled (transsecs.recipemanager=1), the SECS/GEM subsystem will automatically manage S7F1, S7F3, S7F4, S7F5, S7F6, S7F17, S7F18, S7F19, and S7F20 messages.
The file extension for the file based recipes is “rcp”. The directory where the files are stored is where the application is running. So for testing in TransSECS, the recipes will be stored and retrieved from the TransSECS “Builder” directory. When the deployed application is running, the recipes will be stored and retrieved from the directory where the application deployment is running.
These messages allow interaction with the host; however, to fulfill the GEM requirements, the control system must also generate an event whenever a recipe is edited on the tool. For OPCDA Servers, some tags are generated that are used when host messages are received for specific actions.
transsecs.recipedeletenotification=tagname notified when delete(S7F17) is received
transsecs.recipecreatenotification=tagname notified when create(S7F3) is received
If automatic recipe handing is not sufficient, then the recipe messages S7F3, S7F4, S7F5, S7F6, S7F17, S7F18, S7F19, and S7F20 will need to be handled when received. For TransSECS Servers these message parameters are passed to the client software where the recipe message replies are composed and sent. For TransSECS Devices and TransSECS/MIStudio these messages are handled entirely by scripts in the TransSECS application.
To prepare for recipe message handling, add the recipe messages S7F3, S7F4, S7F5, S7F6, S7F17, S7F18, S7F19, and S7F20 to the TransSECS project. You may load the ExtendedTestHost tool into your project and copy/paste the messages from that project to your project, then remove the ExtendedTestHost when finished. All primary messages and replies may be designated bi-directional.
When built, the TransSECS Servers project with the recipe messages will have a .sendMessage tag for each outgoing message (which will be all of them if marked bi-directional). You may restrict the messages you want to handle as incoming, especially if you have no need to send these messages from your TransSECS tool application. When the S7F3 recipe message is received, the data for the PPID and PPBody will be available from the published tags. The reply ACKC7 should be set and the S7F4 sent. When the S7F5 message is received, the PPID value will be available from the published tag. The S7F6 message shoujld be prepared by setting the PPID and PPBody values, then sent using the .sendMessage tag. The list of recipes to delete for the S7F17 message will be set in the corresponding tag. These recipes should be deleted by the client software, if possible and allowed, and then an appropriate ACKC7 set in the S7F18 message, and this messages sent using the .sendMessage tag. For replying to the S7F19, a list of recipe names will need to be written to the recipe name list tag of the S7F20, then the message sent using the .sendMessage tag.
Using TransSECS Devices, the recipe parameters and recipe storage can be PLC or Database based, or a combination of both. For an example of using Database servers for recipe management, see Devices Databases. The examples below are for scripting using PLC device servers.
The example project used below can be downloaded from: Example TransSECS Devices Recipe Project
S7F1
The S7F1 messag (MultiBlock Request) is not strictly necessary but some hosts will send this message before an S7F3 is sent. The tool should handle this message by automatically replying with an S7F2 with a PPGNT value of 0.
Here is how the S7F1 is defined in this example tool
Note that the LENGTH parameter is set to data type <Any> so that any valid 5x or 3x (signed or unsigned) value will match this message definition.
It is set to automatically reply with the “MultiBlock Request ACK” message:
S7F3
This message contains a recipe to be stored or to replace an existing recipe of the same name. When the message is received, the PPID and PPBody values can be written to PLC addresses. Some PLCs cannot store large strings. If this is the case then the PPBody may need to be parsed into specific values which can be stored, one by one, into separate PLC addresses.
Storing recipes in the PLC requires some address space management. Usually this implies a limited number of recipes may be stored. When the S7F3 is received, the recipe space should be searched for that PPID and its PPBody value(s) so that the new PPBody may replace the existing data. If the PPID is new, and there is sufficient space to store the new recipe, the new PPID and PPBody may be stored. The detailed mechanism of indexing and storing the PPID and its PPBody are left to the implementer, and usually set up by the existing PLC program.
If the recipe can be stored, send the S7F4 reply with an ACKC7 of 0. If the recipe cannot be stored, send the S7F4 with the appropriate non-zero ACKC7 code.
The S7F3 example message is defined in TransSECS as:
The S7F4 example message is defined in TransSECS as:
//Basic handling of S7F3 var TransSecsController = Java.type("com.ergotech.transsecs.secs.TransSecsController"); var Logger = Java.type("org.apache.log4j.Logger"); //functions must be defined at the top of the script //reply message function sendReply(ackc7) { print("Sending S7F4 to host with ACKC7: " + ackc7); reply = controller.getMessageBean("ProcessProgramACK"); reply.setACKC7(Java.to([ackc7],"byte[]")); //this is tricky, but correct reply.sendMessage(); } print("S7F3 Received from host"); try { controller=TransSecsController.findController("PLCRecipeTool");//enter the name of your tool project here msgLogger = Logger.getLogger("com.ergotech.transsecs.secs.Log4JLogger"); //this is the logger which logs the SECS/GEM messages msgLogger.info("S7F3 Recieived from host"); //log an info level message to the main log msg=controller.getMessageBean("ProcessProgramSend"); //ProcessProgramSend is the name of the S7F3 message ppid = msg.getPPID(); print("PPID from host: " + ppid); msgLogger.debug("PPID from host: " + ppid); //log a debug message ppbody = msg.getPPBODY(); print("PPBody is "+ppbody); msgLogger.debug("PPBody from host: " + ppbody); //log a debug message //make sure the flags we use to process the S7F3 are set to 0 in the PLC to start /Devices/PLCSimulator_Servers/S7F3RecipeProcessComplete->setIntValue(0); //store the recipe parameters. Use the PLC Device Driver you have set up for your PLC instead of "PLCSimulator" /Devices/PLCSimulator_Servers/S7F3PPID->setStringValue(ppid); /Devices/PLCSimulator_Servers/S7F3PPBody->setStringValue(ppbody); //set the trigger in the PLC which indicates that a recipe has been received /Devices/PLCSimulator_Servers/S7F3RecievedNotification->setIntValue(1); //Let the PLC process the recipe data, wait for S7F3RecipeProcessComplete to be non-zero //wait up to 5 seconds for PLC to process the message data (this time is arbitrary and probably too long) ackc7=0; //default set to failure case flag = /Devices/PLCSimulator_Servers/S7F3RecipeProcessComplete->getIntValue(); //will be 0 to start // /Devices/PLCSimulator_Servers/S7F3RecipeProcessComplete->setIntValue(2); //for testing only (without a real PLC) for (i=0;i<50;i++) { //for the simulator which is a BroadcastServer in PLCSimulator, use getIntValue() to get the value // flag = /Devices/PLCSimulator_Servers/S7F3RecipeProcessComplete->getIntValue(); //for a real PLC, use triggerRead() to get the value flag = /Devices/PLCSimulator_Servers/S7F3RecipeProcessComplete->triggerRead(); //force immediate read from the PLC if (flag>0) { if (flag==99) { //99 indicates success, recipe was correctly processed ackc7=0; } else { ackc7=flag; } break; } java.lang.Thread.sleep(100); //100 ms (this can loop up to 50 times for a total of 5000 ms = 5 seconds) } if (flag==0) { ackc7=88;//PLC did not process the recipe data } //send the reply with the ackc7 code sendReply(ackc7); //now reset the flags in the PLC /Devices/PLCSimulator_Servers/S7F3RecievedNotification->setIntValue(0); /Devices/PLCSimulator_Servers/S7F3RecipeProcessComplete->setIntValue(0); } catch (e) { print("Error in S7F3 script: \n"+e.stack); print(e); }
S7F5
This message contains a PPID requesting that this recipe be returned in an S7F6. If the PPID is not stored in the recipe space of the PLC, then the S7F6 should be returned with an ACKC7 value of 4. Otherwise set the PPBody of the S7F6 message to the complete recipe data value stored in the PLC, then send the S7F6 message.
The S7F5 example message is defined in TransSECS as:
The S7F6 example message is defined in TransSECS as:
and for an empty result (error)
in both of the replies above, the L<> element is named “RecipeParms”.
//S7F5 received var TransSecsController = Java.type("com.ergotech.transsecs.secs.TransSecsController"); var Logger = Java.type("org.apache.log4j.Logger"); //functions must be defined at the top of the script //reply message function sendReply(ppid,ppbody) { print("Sending S7F6 to host"); reply = controller.getMessageBean("ProcessProgramData"); //ProcessProgramData is the name of the S7F6 reply.setPPID(ppid); reply.setPPBODY(ppbody); reply.sendMessage(); } //reply message for empty list (failure case if PPID does not exist) function sendEmptyReply() { print("Sending empty S7F6 to host"); reply = controller.getMessageBean("ProcessProgramDataError");//ProcessProgramDataError is the name of the S7F6 with an empty list reply.sendMessage(); } print("S7F5 Received from host"); try { controller=TransSecsController.findController("PLCRecipeTool");//enter the name of your tool project here msgLogger = Logger.getLogger("com.ergotech.transsecs.secs.Log4JLogger"); //this is the logger which logs the SECS/GEM messages msgLogger.info("S7F5 Recieived from host"); //log an info level message to the main log msg=controller.getMessageBean("ProcessProgramRequest"); //ProcessProgramRequest is the name of the S7F5 message ppid = msg.getPPID(); print("PPID requested: " + ppid); msgLogger.debug("PPID requested: " + ppid); //log a debug message //set the PPID value to the PLC and reset the S7F5DataReady flag /Devices/PLCSimulator_Servers/S7F5PPID->setStringValue(ppid); /Devices/PLCSimulator_Servers/S7F5DataReady->setIntValue(0); //prepare to get the PPBody from the PLC //set the flag in the PLC which indicates that an S7F5 has been received /Devices/PLCSimulator_Servers/S7F5RequestPPBodyFlag->setIntValue(1); //wait a couple seconds for the PPBody value to be set by the PLC (watch for S7F5DataReady flag to go high) flag=0; //default to failure case // /Devices/PLCSimulator_Servers/S7F3RecipeProcessComplete->setIntValue(2); //for testing only (without a real PLC) for (i=0;i<20;i++) { //for the simulator which is a BroadcastServer in PLCSimulator, use getIntValue() to get the value // flag=/Devices/PLCSimulator_Servers/S7F5DataReady->getIntValue(); //for a real PLC, use triggerRead() to get the value flag = /Devices/PLCSimulator_Servers/S7F5DataReady->triggerRead(); if (flag>0) { break; } java.lang.Thread.sleep(100); //100 ms, looped up to 20 times =2000ms (2 seconds) } //if the flag==0 then this did not succeed so need to send an emtpy list S7F6 //if the flag==99 then the PPID did not exist, so need to send the empty reply if (flag!=1) { sendEmptyReply(); } else {//only if flag is "1" //get the ppbody from a simulator (use getStringValue()) //ppbody=/Devices/PLCSimulator_Servers/S7F5PPBody->getStringValue(); //get the ppbody from a real PLC (use triggerRead()) ppbody=/Devices/PLCSimulator_Servers/S7F5PPBody->triggerRead(); print("PPBody for PPID " +ppid+" is "+ppbody); msgLogger.debug("PPBody for PPID " +ppid+" is "+ppbody); //log a debug message sendReply(ppid,ppbody); } //now reset the flags in the PLC /Devices/PLCSimulator_Servers/S7F5RequestPPBodyFlag->setIntValue(0); /Devices/PLCSimulator_Servers/S7F5DataReady->setIntValue(0); } catch (e) { print("Error in S7F5 script: \n"+e.stack); print(e); }
S7F17
This message contains a list of recipes to delete from the PLC. This list may be empty which indicates that all recipes should be removed. Otherwise, the list contains specific PPID(s) for which all recipe data is to be deleted. If one or more of the PPIDs does not exist, send an S7F18 reply with an ACKC7 of 4 and do not delete any recipes. Otherwise delete the specified recipes and send the S7F18 reply with an ACKC7 of 0.
S7F19
This requests a list of available recipes, returns a list of PPIDs in the S7F20 reply.
Using an Array of Registers
This example uses Modbus, but a similar approach works for other PLCs.
In this example create a modbus array that references the recipe registers. Here we read 20 registers starting at 2101
Reading the Recipe from an Array of Registers - S7F5
We're expecting a transaction similar to this. In our PLC all recipe ids (PPIDs) are numeric (1-10)
S7F5 W <A '3'> . S7F6 <L[2] <A '3'> <A '[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,5,0,0,1]'> > .
var TransSecsController = Java.type("com.ergotech.transsecs.secs.TransSecsController"); var ValueChangedEvent = Java.type("com.ergotech.vib.valueobjects.ValueChangedEvent"); var LongValueObject = Java.type("com.ergotech.vib.valueobjects.LongValueObject"); /* Check recipe number (must be 1-10) Read 2101-2120 for the values of the ppbody */ print ( "S7F5 Received" ); secsInterface=TransSecsController.findController("PLCRecipeTool"); recipeFromHost=secsInterface.getMessageBean("ProcessProgramRequest"); ppid = recipeFromHost.getPPIDFormat().getIntValue(); print ("Request to store recipe " + ppid); if ( ppid < 1 || ppid > 10 ) { reply = secsInterface.getMessageBean("ProcessProgramDataDenied"); reply.sendMessage(); } else { /Devices/ModbusTCP_Servers/PPID->setIntValue(ppid); // populate the PPID to get an update of the recipe registers // trigger a read of the recipe registers. /Devices/ModbusTCP_Servers/RecipeArray->triggerValueChanged(new ValueChangedEvent(this,new LongValueObject(1))); arrayValueObject = /Devices/ModbusTCP_Servers/RecipeArray->getValueObject(); print ("Register Data " + arrayValueObject); var values = []; for ( index = 0 ; index < 20 ; index++ ) { values[index] = arrayValueObject.get(index).getIntValue(); } recipeJson = JSON.stringify(values); reply = secsInterface.getMessageBean("ProcessProgramData"); reply.setPPID(ppid); reply.setPPBody(recipeJson); print ( "Recipe Values: \"" + recipeJson + "\"" ); reply.sendMessage(); }
Write the Recipe to an Array of Registers - S7F3
We're expecting an S7F3 with an array of 20 values as JSON string, for example, this:
S7F3 W <L[2] <A '2'> /** PPID */ <A '[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 5, 0, 0, 1]'> /* PPBODY */ > .
var TransSecsController = Java.type("com.ergotech.transsecs.secs.TransSecsController"); var LongValueObject = Java.type("com.ergotech.vib.valueobjects.LongValueObject"); var ArrayValueObject = Java.type("com.ergotech.vib.valueobjects.ArrayValueObject"); var ValueChangedEvent = Java.type("com.ergotech.vib.valueobjects.ValueChangedEvent"); /* Check recipe number (must be 1-10) Populate PPID with recipe number Populate 2101-2120 with values from ppbody */ print ( "S7F3 Received" ); secsInterface=TransSecsController.findController("PLCRecipeTool"); reply = secsInterface.getMessageBean("ProcessProgramAck"); recipeFromHost=secsInterface.getMessageBean("ProcessProgramSendFromHost"); // add validity checking for the recipes // plc recipes are frequently numeric. Here we'll allow only recipes 1-10 ppid = recipeFromHost.getPPIDFormat().getIntValue(); print ("Request to store recipe " + ppid); if ( ppid < 1 || ppid > 10 ) { reply.setACKC7(Java.to([4],"byte[]")); // PPID not found reply.sendMessage(); } else { /Devices/ModbusTCP_Servers/PPID->setIntValue(ppid); // set the ppid arrayValueObject = new ArrayValueObject(); // create an object to write to the array // the values are expected to be in the format of an array, and we're expecting 20 eg [1,2,3,....20] (the brackets are part of the received value var values = JSON.parse(recipeFromHost.getPPBody()); print ( "Recipe Values: \"" + values + "\"" ); for ( index = 0 ; index < 20 ; index++ ) { arrayValueObject.add(new LongValueObject(values[index])); // create the value object } // set it to the Modbus Array arrayValueObject = /Devices/ModbusTCP_Servers/RecipeArray->valueInput(new ValueChangedEvent(this,arrayValueObject)); print ("Register Data " + arrayValueObject); reply.setACKC7(Java.to([0],"byte[]")); // PPID not found reply.sendMessage(); }
The scripts in MIStudio will be very similar to the scripts for TransSECS Devices, depending on the data sources and whether PLCs are being used for the recipe parameters or if database tables are being used.
For Programmatic TransSECS, set up message handlers for each incoming message. Within the message handler, process the message, then set the data values for the reply, and send the reply (sendMessage).