
You already know how to add new functions, function blocks and data types using the script. It is easy - just import the related text files. But this is somehow limited. You can introduce new building blocks only based on the existing ones. How about adding a new block that relies on an external API? Is this possible?
Yes. This is what we do when exposing the Process Simulate API to the script. Some people think that we extend the language grammar by adding a new block but we rather rely on the existing grammar and extend the semantics (just like with the sub scripts). If we change the grammar then it will be a different language that we use, not SCL.
So how we do it?
To introduce a new block in the script we must make it recognize the block somehow.
We need a way to register the new block in the script engine. Then when the script runs and calls this block at some point, we need a way to redirect the call to this block so it can do its job. The blocks must be loaded upon request (lazy loaded) so we save the time for loading blocks that the code does not use.
Tecnomatix.Engineering.Scripting.SCL.dll contains the API that allows the SCL interpreter to do all this. It is an interop dll that brings native COM interfaces to .Net. Why interop? Scripting is primarily a low level thing (C++ in our case) the interop brings it to the managed world.
Let's introduce you to the basic abstractions:
Script library - entity loaded by the script engine. allows the script engine to enumerate and get instances of the POUs inside.
POU factory - allows the script engine to create FB/UDT instances upon request
FC - POU implementing the interface for a FC
FB - POU implementing the interface for a FB
UDT - POU implementing the interface for a UDT

Your role (as a developer) is to provide the script engine with objects that implement specific COM interfaces. To do so you must start with the script library. What does this mean? First you create a dll project, add a reference to the
Tecnomatix.Engineering.Scripting.SCL.dll, modify the AssemblyInfo.cs and then create a class like this:
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("4826e69b-1681-4550-a256-bfbfcdb25a78")]
[assembly: ComVisible(true)]
using System.Runtime.InteropServices;
using Tecnomatix.Engineering.Scripting.SCL;
namespace mylib
{
[
ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual),
Guid("EEAF5BFA-E4B4-4980-8EC1-D42A3C71C993")
]
public class MyLibrary : ISCLScriptLibrary
{
…
}
}
It is important that the Guid you use for the script library and the COM assembly are unique.
Always generate new ones using Guid generator in Visual Studio (Tools->Create GUID). Make sure the class is public! If it is not the class won't be visible for COM.
You can leave the interface methods empty for now and compile the code in 64 bit.
Next you should register the assembly with COM. Open a console window and run the 64bit version of regasm.exe (it comes with the .Net framework):
regasm.exe /codebase <path to your dll>
Once this succeeds all that is left is to tell the SCL script engine to load the script library. You need a registry key for this. The Guid of the script library should be the key. The value is an arbitrary string. Usually the name of your library.
[HKEY_CURRENT_USER\Software\TECNOMATIX\TUNE\Scripting\SCL\Extensions\{EEAF5BFA-E4B4-4980-8EC1-D42A3C71C993}]
@=“MyLibrary"
The key can also be placed under HKEY_LOCAL_MACHINE as well but you need admin privileges for this.
So far so good. At this point your library will be loaded by Process Simulate whenever Process Simulate creates an instance of the script engine. This happens on simulation play or when running the code from the SCL editor.
Now we can implement a function. This is simple. Just create a class like this one:
using System.Runtime.InteropServices;
using Tecnomatix.Engineering.Scripting.SCL;
namespace mylib
{
[
ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual)
]
public class MyFC : ISCLFunctionHandler
{
…
}
}
As mentioned earlier the script library enumerates the POUs. To make the function visible for the script engine implement the methods inside the script library:
IEnumerator ISCLScriptLibrary.EnumPouNames()
{
string[] pouNames = {
“MyFC",
};
return pouNames.GetEnumerator();
}
dynamic ISCLScriptLibrary.GetPou(string pouName)
{
if (String.Equals(“MyFC", pouName))
return new MyFC();
return null;
}
Recompile and start Process Simulate. You should be able to see the new function in the SCL Editor's auto complete popup.
To call the function you need to provide metadata for the arguments. e.g. provide the type info for the function. It is just an object implementing ISCLTypeInfo interface. Arguments are represented by the ISCLPropertyInfo interface.
[
ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual)
]
public class PouTypeInfo : ISCLTypeInfo
{
...
}
[
ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual)
]
public class PouArgInfo : ISCLPropertyInfo
{
...
}
public ISCLTypeInfo ISCLFunctionHandler.TypeInfo
{
get
{
return new PouTypeInfo(“MyFC", _arguments.ToArray());
}
}
Usually you build the arguments list in the constructor of your function and simply return it as part of the type info. How do we manifest the type of the return value of a function? Easy. It is one of the arguments actually ... seems weird but that's life :)
Once you provide the type info you will see the argument types/return value type in the auto complete popup when you try to call the function.
Last when the function is called you need to provide the logic behind it.
You just have to implement a single method:
public dynamic ISCLFunctionHandler.Call(string[] argNames, object[] argValues)
{
...
}
Some things to remember:
Because you provide metadata about the arguments the SCL script engine will make sure that all input arguments are passed with the type you manifest. e.g. if you have a INT input and pass a REAL value to it, the value will be cast to INT first. If the cast fails you'll get an error.
Because you provide metadata about the arguments the SCL script engine will make sure that all output arguments are passed by reference and the reference points to a variable of the type you manifest. e.g. if you have a INT output the variable that is passed should be of the same type. Otherwise you'll get an error.
Because you provide metadata about the return value you must make sure that the return value is of the type you manifest. If you don't you might get an error. If you don't manifest the return value type and you return a value, you won't be able to use the function in expressions.
Until now it all seems quite good. Simple coding with big results.
Now the bad news:
COM interop calls are a good but cannot represent arguments passed by reference. The .NET framework resolves the native references and presents them as regular values instead. For us this means that output arguments are not possible in .NET. They only work in C++. This is a major downside that we could not overcome!
Note: for FBs there is a workaround - use the output like a property instead of an argument
Implementing the script library in C++ is possible (we do it internally for FMUs for example) but for users it is not an option. Process Simulate API is based on .NET ...
If you use the .NET API you'll face an issue locating the Process Simulate assemblies at runtime when referenced from the script library. One way to solve this is to place the script library dll in the PS installation folder. Otherwise the assemblies need to be in the GAC which is not an option for now.
The bottom line is that scripting (low level code) and .NET (high level code) are not meant to coexist but we had to make them somehow work together.
Function blocks are similar to functions. Again you create a class implementing specific interface but this time you need two of them - factory class and a function block class:
using System.Runtime.InteropServices;
using Tecnomatix.Engineering.Scripting.SCL;
namespace mylib
{
[
ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual)
]
public class MyFB : ISCLFunctionBlockHandler
{
…
}
[
ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual)
]
public class MyFBFactory : ISCLPouFactory
{
SclPouType ISCLPouFactory.PouType
{
get
{
return SclPouType.FunctionBlock;
}
}
object ISCLPouFactory.CreateInstance(string initString)
{
return new MyFB();
}
}
}
The implementation is similar to functions. This time you return the factory from the script library:
IEnumerator ISCLScriptLibrary.EnumPouNames()
{
string[] pouNames = {
“MyFB",
};
return pouNames.GetEnumerator();
}
dynamic ISCLScriptLibrary.GetPou(string pouName)
{
if (String.Equals(“MyFB", pouName))
return new MyFBFactory();
}
UDTs are quite the same as FBs but need serialization. This time the interface you implement is ISCLUserDefinedDataTypeHandler. The serialization part however is not trivial for everybody. This is why we provide a way to define them using the SCL syntax. The script engine parses the syntax and provides the implementation for you. You just return the definitions from the script library:
string ISCLScriptLibrary.UdtDefinitions
{
get
{
// definition of the "Point" UDT
return @"TYPE ""Point""
VERSION: 0.1
STRUCT
x:LREAL; // x position
y:LREAL; // y position
END_STRUCT;
END_TYPE
(* Other UDT Definitions Go Here *)";
}
}
FCs and FBs can optionally implement the ISCLContextAwareHandler interface.
As the name suggests the POU can be made aware of the context where it runs. What does this mean? The context is an object (implementing ISCLExecutionContext) that provides some information to the POU:
The reference clock - provides the simulation time (ms)
The script engine - the engine that runs the script
The resource identifier (apiId) - clear enough this is the resource owning the code
The update rate (updateRate) - the sampling rate for the script (ms)
Design mode flag (designMode) - the script runs from the SCL editor
The last three properties are opaque to the scripting so they are provided as key-value pairs over the standard IPropertyBag interface:
public dynamic ISCLContextAwareHandler.ExecutionContext
{
set
{
IPropertyBag ctx = value as IPropertyBag;
if (ctx != null)
{
object id;
ctx.RemoteRead("apiId", out id, null, 0, null);
_resource = TxConvertor.ITxObjectFromIdString(
(string)id) as ITxLocatableObject;
}
}
}
The context is provided once before the first call to the FC/FB.
Lastly let's take a look at the ISCLDisposableHandler interface. As the name suggests it is designed to dispose native resources when the script engine is destroyed. This may include freeing up allocated memory, disconnecting events etc. It is quite useful especially with .NET implementations where the lifetime of the objects differs from the ones in C++.
Now you know how the POUs are being registered and implemented. How about lazy loading? It is easy. During simulation the script engine is made aware of all script libraries but only referenced POUs are actually acquired. Nice!
Let's get back to the scripting approach to define FCs/FBs. Believe it or not scripts work based on the same API. We use the script info (mentioned in a previous post) to provide the type info for the handlers. When 'called' each handler parses the code and runs it. Simple!
See you!
Note: We provide some sample code in your installation folder under TecnomatixSDKHelp\Samples\SCL\API. Try it!
Comments