LogixNG has native support for complex calculations with the tool "Formula". Formula supports almost all the Java operators and you can use local variables and functions with formula. In many cases, like the action Turnout, you can choose to use formula to get the turnout you want to act on, or to get the new state you want to set.
Local variables, which are explained in chapter 8, can be used directly in formula. So if you have a local variable index, you can for example have the formula "IT" + str(index), which adds the string "IT" and the value of index. This can be useful if you for example want to set all the turnouts IT1, IT2, IT3, ..., IT10 to thrown. You can then use the For action to iterate from 1 to 10 and to set each of these turnouts to thrown.
There are three expressions for formula: Analog Formula, Digital Formula and String Formula. They all work the same way, except that Analog Formula returns a floating point number, Digital Formula returns a boolean value (true or false), and String Formula returns a string. The expression Formula can have child expressions, for example reading an analog value or reading the state of a sensor. You use the result of the child expressions by using the name of the female socket in the formula. So if you have an expression Formula which has a child E1 to which an expression Sensor is connected, you can use the result of the expression Sensor in the formula by the identifier E1 which points to the female socket and its connected expression.
Formula supports most of the Java operators. A list of the Java operators, together with the priority of them, is on this page.
Currently supported operators are:
Operator | Description | Associativity |
---|---|---|
v++ v-- |
post-increment post-decrement |
not associative |
++v --v |
pre-increment pre-decrement |
right to left |
- | unary minus | right to left |
! | unary logical NOT | right to left |
~ | unary bitwise NOT | right to left |
* / % | multiply, divide, modulo | left to right |
+ - + |
additive string concatenation |
left to right |
<< >> >>> |
shift left shift right unsigned shift right |
left to right |
< <= > >= |
relational | not associative |
== != |
equality | left to right |
&& | logical AND | left to right |
^^ | boolean XOR | left to right |
|| | logical OR | left to right |
?: | ternary | right to left |
= += -= *= /= %= &= |= ^= <<= >>= >>>= |
assignment | right to left |
Note that for the calculations to work, each operand must have the correct type. For
example, if you have a local variable MyVar that has a number as a string and
you want to subtrack from it, like MyVar - 1
, you need to convert the string in
MyVar to an integer or a float. Example: int(MyVar) - 1
or float(MyVar) - 1
. The same rules apply to concatenating a string and an integer. The integer has to be
converted to a string, such as "IT" + str(index)
In this example, the A1 section uses the For action to toggle 5 turnouts. The turnout user names are T1 thru T5. The index local variable is used to supply the number. The index value has to converted to a string before concatenating with the "T"
The A2 section is an example of a digital expression. This a simple one with just the "and" operator.
Formula supports functions, like sin(x) and random(). Some functions takes one or several parameters. A function has an identifier, for example "sin", followed by a left parentheses, optional one or several parameters separated by comma, and then closed by a right parentheses.
The dialog boxes for editing an action or expression, and the dialog box for editing variables, has a button "Formula functions". If you click on that button, you get a new dialog box that shows the functions that are available and the documentation on each of them.
For JMRI developers: The functions are defined in the package jmri.jmrit.logixng.util.parser.functions and each module has its own Java class. Each function is its own class that implements the Function interface.
To make it easier to use the functions, each function has some documentation.
Each action/expression has a Formula functions button that opens a dialog box with documentation of the functions.
The functions are grouped in modules to make it easier to find the functions. Select the module you are interested in.
Then select the function you are interested in.
In this case, the function fastClock() take a string parameter which can have any of the values hour, min or minOfDay.
Some functions, for example the function random(), can take different
numbers of parameters. The default is 0.0 <= x < 1.0
. Supplying
max or min and max values can change the range.
Sometimes it is possible to access Java methods. For example, for most of the JMRI beans it is possible to access the public methods in the bean's Interface. This can also apply to some of the main Java classes, such as String.
Some Java methods return a Java Array instead of a List. The LogixNG Formula has been enhanced to work with a Java Array.
Here is an example using the Java array created by the String split method.
LogixNG: IQ:AUTO:0001 ConditionalNG: IQC:AUTO:0001 ! A Many ::: Local variable "a_b", init to String "Hello_World" ::: Local variable "a", init to None "" ::: Local variable "b", init to None "" ::: Local variable "a_b_array", init to None "" ! A1 Digital Formula: a_b_array = a_b.split("\_") ! A2 Digital Formula: a = a_b_array[0] ! A3 Digital Formula: b = a_b_array[1] ! A4 Log local variables
It's possible to create a new function using Jython to be used by a formula. The code below gives an example that you can use as a template. A new function is added by creating a new class that extends the class Function and implements these methods:
Method | Description |
---|---|
getModule | The name of the module that the function belongs to |
getName | The name of the function |
getDescription | Description of the function for the user |
getConstantDescriptions | Description of any constants |
calculate | Calculate the function |
Example Jython script that defines the getBlockValue function that takes the name of a block as its parameter:
import jmri class BlockValue(jmri.jmrit.logixng.util.parser.Function): def getModule(self): return 'Custom' def getName(self): return 'getBlockValue' def getDescription(self): return 'Get the current value for the specified block name.' def getConstantDescriptions(self): return None def calculate(self, symbolTable, parameterList): if (parameterList.size() != 1): raise jmri.jmrit.logixng.util.parser.WrongNumberOfParametersException("Function requires one parameter") blockName = parameterList.get(0).calculate(symbolTable) block = blocks.getBlock(blockName) return None if block is None else block.getValue() jmri.InstanceManager.getDefault(jmri.jmrit.logixng.util.parser.FunctionManager).put("getBlockValue", BlockValue())
LogixNG: Test Script Function ConditionalNG: Get Block Value ! A Many ::: Local variable "BlockValue", init to None "" ! A1 Digital Formula: BlockValue = getBlockValue("TestBlock") ?* E1 ! A2 Log local variables
The function calculate takes a number of arguments as a List<ExpressionNode>. We first check the number of arguments by calling the method size() and if that's correct, we get the arguments by calling the method get(index) where "index" is the index of the argument.
But to do something useful with the arguments, we need to calculate each argument we want to use. We do that by calling the method calculate on each argument we want to use.
We then do the calculation, which in this case finds the requested block and returns the current value.
Since the local variables symbol table is also passed to the calculate function, local
variables are also available to the script using symbolTable.getValue(name)
and
symbolTable(name, new value)
.
The Jython script needs to loaded before the LogixNG runs that references the custom function. The best approach is to add a start up action to run the script. When the xml data file is loaded, the script will be ready to process the custom function request.
A function may set turnouts, sensors, and other things on the layout. You may for example create the function setTurnout(turnout, newState). But it's important to remember that a ConditionalNG runs on a separate thread so if you set a turnout or a sensor, you must do that on the layout thread. Formula is always run on the thread that the ConditionalNG is run on, so if a function updates the layout or the GUI, it needs to do it on the layout thread or the GUI thread.
See the later chapter on threads (may not exist yet) for more information of LogixNG threads.
Select a suitable module for the new function. Each module has its own class and most module classes reside in the jmri.jmrit.logixng.util.parser.functions package, although it's possible to put a module in another Java package if desired. If a module is aimed for an particular part of JMRI, for example LocoNet, it might be better to put it in the jmri.jmrix.loconet.logixng package.
The module has a getFunctions() method which tells the FunctionManager which functions each module provides. This method returns a set of functions. To create a new function, add a new anonymous class to this set. Have the new class extend the AbstractFunction class.
The AbstractFunction constructor takes three parameters. The module, the name of the function and the description of the function. The anonymous class then needs to implement the calculate() method which has the actual implementation of the new function.
The calculate() method takes two parameters, the symbol table and the parameters to the function. It then does the desired calculation for the function and then returns the value. parameterList.size() gives the number of parameters for the function. parameterList.get(index).calculate(symbolTable) returns the value of parameter index.
The jmri.util.TypeConversionUtil class has some useful utility methods to convert the parameters to some types, for example String, long and double.
Example: The sqrt() function in the Math module. The addSqrtFunction() method is called from the MathFunctions.getFunctions() method.
private void addSqrtFunction(Set<Function> functionClasses) { functionClasses.add(new AbstractFunction(this, "sqrt", Bundle.getMessage("Math.sqrt_Descr")) { @Override public Object calculate(SymbolTable symbolTable, List<ExpressionNode> parameterList) throws JmriException { if (parameterList.size() == 1) { double param = TypeConversionUtil.convertToDouble(parameterList.get(0).calculate(symbolTable), false); return Math.sqrt(param); } else { throw new WrongNumberOfParametersException(Bundle.getMessage("WrongNumberOfParameters1", getName())); } } }); }
Thanks and congratulations to all who contributed! Contact us via the JMRI users Groups.io group.
Copyright © 1997 - 2024 JMRI Community. JMRI®, DecoderPro®, PanelPro™, DispatcherPro™, OperationsPro™, SignalPro™, SoundPro™, LccPro™, TrainPro™, Logix™, LogixNG™ and associated logos are our trademarks. Additional information on copyright, trademarks and licenses is linked here.
View the