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.

- Clock
- Common
- Convert
- Javasince 5.1.3
- Layoutsince 4.25.8
- Math
- NamedBean
- String

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™, TrainPro™, Logix™, LogixNG™ and associated logos are our trademarks. Additional information on copyright, trademarks and licenses is linked here.

View the