Custom Function Implementation

Hello everyone,

My company want me to implement a few custom expression functions to Ignition as shown below:

).

However, I am new to Ignition and unfortunately, I can’t find out how to add the custom expression functions with the help of the documentation. As far as I know, I have to first extend the AbstractGatewayModelHook.

After that, the problem(s) starts:

  1. How to get access to the “expression” functions drop down list?

  2. How to add the “custom” functions within the existing function drop down list?

  3. How can I “import” the custom code to the tool? In mean in the documentation it is described, that the gateway can load a .modl file, but where do I find this gateway within the tool?

Thank you very much in advance for help.

Your best bet is to start by becoming familiar with Ignition, because you’ll need to understand the difference between the Gateway, Designer, and Client before you can start writing custom modules. The free courses at Inductive University are a great place to start.

Once you’re familiar with Ignition, work through compiling and installing one of the example SDK modules, so you can see how to build, sign, and load a module without worrying about adding new code. Until you’ve mastered those steps, it’s a bit early to worry about #1 and #2 in your question.

1 Like

Just to add some clarification to Kathy’s comment: Writing custom expression functions requires writing and compiling Java code and distributing it in a .modl file. Not only can your not do this with jython, the expression function system is almost totally separate from the jython scripting system.

Hello Kathy and pturmel.

Thank you very much for your answers. I will take a look at the video materials you linked, but I am not sure if they help me with my specific problem. pturmel, I am aware, that you need Java development knowledge, which is not the problem. In addition, I got a sample code from one of the inductive automation Sales Engineers.

Gateway:

public class GatewayHook extends AbstractGatewayModuleHook {

private final Logger logger = LoggerFactory.getLogger(getClass());
private GatewayContext gatewayContext;
@Override
public void setup(GatewayContext gatewayContext) {
    this.gatewayContext = gatewayContext;
}
@Override
public void startup(LicenseState licenseState) {
}
@Override
public void shutdown() {
}
@Override
public void configureFunctionFactory(ExpressionFunctionManager factory) {
    factory.addFunction(ConvertBase.FUNC_NAME, ConvertBase.CATEGORY, new ConvertBase(gatewayContext));
}

}

Convert Base:

public class ConvertBase implements Function {

public static String FUNC_NAME = "ConvertBase";
public static String CATEGORY = "FSQL_Tools";
private GatewayContext context;
public ConvertBase(GatewayContext context) {
    this.context = context;
}
public static void main(String[] args) {
	System.out.println(CATEGORY);
	System.out.println();

}

@Override
public QualifiedValue execute(Expression[] expressions) throws ExpressionException {
    QualifiedValue from = expressions[0].execute();
    QualifiedValue to = expressions[1].execute();
    QualifiedValue numberToConvert = expressions[2].execute();
    Quality quality = DataQuality.worstOf(from.getQuality(), to.getQuality());
    quality = DataQuality.worstOf(quality, numberToConvert.getQuality());
    Integer inputInt = Integer.valueOf(numberToConvert.getValue().toString(), (Integer) from.getValue());
    String output = Integer.toString(inputInt, (Integer) to.getValue());
    return new BasicQualifiedValue(output, quality);
}
@Override
public void initArgs(Expression[] args) {
    if (!validateNumArgs(args.length)) {
        throw new RuntimeException("Function '" + FUNC_NAME + "(" + getArgDocString()
                + ")' does not work with " + args.length + " arguments.");
    } else {
        for (int i = 0; i < args.length; i++) {
            if (!validateArgType(i, args[i].getType())) {
                throw new RuntimeException("Type mismatch: Argument " + i + " for function '" + FUNC_NAME
                        + "(" + getArgDocString() + ")' is an invalid type.");
            }
        }
    }
}
protected boolean validateNumArgs(int num) {
    return num == 3;
}
protected boolean validateArgType(int argNum, Class<?> type) {
    boolean ret = false;
    switch (argNum) {
        case 0:
            ret = type == Integer.class;
            break;
        case 1:
            ret = type == Integer.class;
            break;
        case 2:
            ret = type == String.class;
            break;
    }
    return ret;
}
@Override
public Class<?> getType() {
    return String.class;
}
@Override
public String getArgDocString() {
    return "from, to, numberToConvert";
}
@Override
public Function copy() {
    return this;
}
@Override
public void connect(BaseContext baseContext, InteractionListener interactionListener) {
}
@Override
public void disconnect() {
}
@Override
public void startup() {
}
@Override
public void shutdown() {
}

}

However, I wonder how to test if the code works as he told me it would. This leads back to my question, How to transform his code (which I have in Eclipse) to this curious "modl" file and tell Ignition to apply the changes?

Thank you very much for your help.

As Kathy said, you need to use the SDK, linked from the online docs. You must set up maven in your dev environment and create a module properties file. You may also need to create code for the client and designer scopes, if you want your new function to work there as well. You will need a java code signing certificate, or a self-signed one. When you finally get a .modl file built, you use Ignition's web gateway interface to install it into your running server.
I can't possibly list all the steps and nuances here -- that's why IA has a git repo with a bunch of examples for various scenarios. (Linked in the SDK Docs.) The code snippet you were given presumes you've accessed that and understood how a module is structured.

Hi Phil,

You are totally right! However, I read that manuel. At page 13 it is "described" how to form an .modl in the following way:

Compile and Deploy
Now all of your projects should be in Eclipse and it will compile (or "build") all of them. You'll notice
that one of the projects is called "Build". This project has all of the Ant scripts that are used to
compile and assemble the Java code into an Ignition *.modl file. Each project has its own build file.
For example, the component example projects have a build file called build-componentexample.
xml. Double click on this Ant file to open it in Eclipse's Ant editor. (If it opens in a raw
XML editor, close that, and then right click on it and choose Open With > Ant Editor). You'll see in
Eclipse's Outline view each of the Ant script's Targets. These are what you'll use to compile your
module. You can right-click on the "build" target and choose Run As > Ant Build to build that
module file. If you have a Developer Mode Gateway running locally, you can run the "deploy" target
to push your newly built module to your Gateway.
That's it! You're up and running. You've compiled and deployed a module using the SDK. Now you
can write your own module or adapt one of the examples to create new functionality for Ignition.

However, there are several problems:

  1. Where are theses "Ant scripts" located in the "build" project? I have an ExpressionFunctions-build project, but don't see any of theses scripts.

  2. I don't see any "...build.xml" files in any of my projects. I have some Expression-Functions-build.iml files!

  3. The rest of the text I don't need to mention, because step 1. and 2. have to be solved first.

Thank you for help.

I wrote a blog post that may be helpful in getting a basic module built and installed.

2 Likes

Thank your fourmajor for the blog post. It was very helpful :slight_smile: I was able to install the above module. However, I can’t find the “new” function. Does anyone have an idea what went wrong? The logs does not tell that any errors occurred.

Log: Starting up module ‘com.paulwurth.expressionfunctions’ v1.0.0 (b0)…

In addition, this picture shows, that it was successfully installed:

Thank you

If you only did a gateway hook, you’ll only have the new function in tag expressions, and you’ll have to enter it blind, as the designer won’t know about it.

Hi Phil,

Thank you for your answer but I don’t know what you mean by that? The code has a gateway hook which implements the abstract gateway and the convertbase that implements a function.

What else do I need to see the function then appearing somewhere?

Thank you

If you want to see your function in the Designer, you need to also have a designer hook that implements your function.

Again, I’ll suggest that you watch some of the free videos at Inductive University, especially the gateway and the designer videos so that you know the difference between them.

Hello Kathy,

Thank you very much for your answer. However, I don’t understand what you mean by an “designer hook” that implements the function. Where do I have to add that class in my project structure? Can you please clarify that up, or provide me an example of such a designer hook? Here is the full project example I have until now: PaulWurthExpressionFunctions.zip (77.5 KB)

P.s. I watched all the video materials already, and they don’t help a single bit because they do not have anything to do with developing your own module. Never the less, thank you for that.

Consider reviewing the structure of the scripting-rpc example module. It has all three scopes and the typical project structure for producing it.

Hi Phil,

Thank you very much for the answer. However, the example is not even working because there is a missing jar… eclipse/milo/sdk-client/0.1.0-SNAPSHOT/sdk-client-0.1.1-SNAPSHOT.jar.

I found this post, but my pom file does not look like JMcguigan pom and hence I can’t fix the issue in the same way. So how should I fix the issue to test the example so that I may understand how I can make my example run?

As you might have noticed from my comment in that thread, I can’t help you with maven or the proper resolution of dependencies in its pom files. (I still use ant for my modules.) My point was that deploying a custom function generally requires a multi-scope project (Gateway, Designer, Client) and the scripting-rpc project shows how to divide up and organize your code.

It sounds like you might be using an older version of the SDK, so you may benefit from updating the versions in your pom.xml to 7.8.5, or adding the the repo posted in the post you reference to your <respositories> list.

The problem you are seeing results from the use of an earlier dependency on the milo library, which was published to the sonatype snapshots, and not yet maven central (as it was pre-release at that point).

In terms of the scope issue, Kathy and Phil are leading you down the right path in figuring out how to add your expressions to the designer. Your expression should already run on gateway when saved and executed, but the designer knows nothing about them. In order to make the designer aware, you also need to add a designer-scoped subproject to your module as the example Phil linked demonstrates.

Thank you Perry for your answer,

  1. You are right, I had to use 7.8.5 instead of the predefined 7.9.0-SNAPSHOT. However, to be honest, examples should be correct from the beginning on and this should be fixed so that no one else encounter this kind of problem again.

  2. I am aware that they want to lead me the right way. However, it would be much easier and faster if the example function would at least show up in the designer so that I could finally start with the implementations of the custom functions. Because all this steps leads to addition problems. Now, I think I have the “structure” as it should be but I still can’t see the function within ignition. Any other ideas or is still something wrong?PaulWurthExpressionFunctions (new).zip (78.8 KB)

Thank you for help.