Skip to content

MathForward

Michiel TJampens edited this page Nov 3, 2022 · 12 revisions

Being a forward means that it can receive data from one or more sources do something with it and provide the resulting data as a source for multiple targets.

This particular forward:

  • Must receive delimited data
  • Will apply mathematical operations on the data after splitting it
  • Can send commands

Purpose

Split data received according to a chosen delimiter and alter the numerical elements using mathematical operations.

Main features

  • Supports addition(+), subtraction(-), multiplication(*), division(/), remainder(%) and power(^).
  • Can apply rounding
  • The rule of 'order of operations' is followed with the exception that % is below * and / instead of equal.
  • All operations are executed for accuracy first and performance second (java double versus bigdecimal)
  • Numbers can be in either decimal, hexadecimal or scientific notation.
  • The delimiter can be any ascii string, in the future hex will be implemented.
  • If there are no targets then it won't receive data from its sources (eg. no overhead if not used)
  • Use the result of an operation as part of a command (see cmd)

Use case

The GGA string received from a GPS contains coordinates in degrees minutes while degrees are desired.

Mathematical solution

Consider the following data received from a GPS:
$GPGGA,095835.147,5104.24631,N,00227.67672,E,1,,,-3.6,M,,,,*0E

Splitting this on , and numbering them from i1:
i0 - $GPGGA
i1 - 095835.147 = time of gps fix
i2 - 5104.24631 = latitude 51° 4.24631' i3 - N
i4 - 00227.67672 = longitude 2° 27.67672' i5 - E
etc

Latitude and longitude are formatted in degrees minutes, decimal degrees are easier to use so we'll convert them. To do this we need to extract the degrees part and add it to the minutes part divided by 60.

Which is done with (i2-i2%100) to get the degrees part and (i2%100)/60 to get the minutes and convert it to degrees. Putting those two together means that (i2-i2%100) + (i2%100)/60 will be the new i2.

Or i2 = (i2-i2%100) + (i2%100)/60 or i2=(5104.24631-5104.24631%100) + (5104.24631%100)/60 = 51.7077183

This will be used to explain MathForward.

MathForward solution

The Easiest way is using a command through the telnet interface.

mf:addblank,id,source Create a blank math in the xml with the given id and optional source

Applied to the problem, this becomes 'mf:addblank,degrees,raw:gps' will add the following node to the settings.xml.
Degrees is the id we use to refer to the operations and data is the raw data from the gps.

Note: raw:gps is explained in the Streampool section.

<maths>
    <!-- Some info on what the test math does -->
    <math delimiter="," id="degrees" src="raw:gps">
        <!-- Operations go here, possible types: complex, scale -->
    </math>
</maths>

Next up is adding the operations to it, this can be done with another telnet command: mf:addop,id,operation Add the given operation to the math with the provided id

Or by adding a default operation with mf:addop,id and then editing this in the xml file and then use mf:reload,id to reload/apply it.

So executing: mf:addop,degrees,i2 = (i2-i2%100) + (i2%100)/60 to add the latitude mf:addop,degrees to add a default one for the longitude

Below is the result of both addop commands

<!-- Some info on what the degrees math does -->
<math delimiter="," id="degrees" src="raw:id:gps">
      <!-- Operations go here, possible types: complex, scale -->
      <op>i2=(i2-i2%100) + (i2%100)/60</op> <!-- latitude -->
      <op>.</op>   <!-- ready for longitude -->
</math>

In the end, if longitude is filled in, the result would be: $GPGGA,095835.147,51.7077183,N,2.46127866,E,1,,,-3.6,M,,,,*0E

Reference Guide

Commands

mf:addblank,id,source Create a blank math in the xml with the given id and optional source
mf:addop,id,index=equation Add an operation to the given math that writes the result to the given index
mf:addsource,id,source Add the given source to the id
mf:reload,id Reload the given id

mf:list List all the available maths and their operations
mf:debug,on/off Turn debugging on or off this will show how the op's are executed
mf:test,id,data Give data to test it

Additional functionality

Defaults

If an attribute is considered default, it can be omitted from the node.

  • The default delimiter is ','
  • The default type attribute of operation is 'complex'
  • The default label is empty

Short notation If the math only contains a single op, it can be written like this:

<math id="degrees">i2=(i2-i2%100) + (i2%100)/60</math>

Using realtime data If the realtimevalues contain a double named offset, this can be added to the expression like this:

<math id="degrees">i2=(i2-i2%100) + (i2%100)/60 + {d:offset}</math>

Or if you want to save the result of the op in such double, this can be done like this:

<math id="degrees">{d:latitude},i2=(i2-i2%100) + (i2%100)/60</math>

Or

<math id="degrees">i2,{r:latitude}=(i2-i2%100) + (i2%100)/60</math>

If you don't care about i2 in the following steps, it can be omitted

<math id="degrees">{r:latitude}=(i2-i2%100) + (i2%100)/60</math>

Flags can also be referenced, using {f:flagid} but NOT on the left side of the =. On the right these will be replaced with either 1 or 0.

Multiple sources

Multiple sources is possible and this is how this looks in the node, but no use case for this yet...

<math id="degrees">
      <src>raw:id:gps</src>
      <src>raw:id:rtk</src>
      <op>i2=(i2-i2%100) + (i2%100)/60</op> <!-- latitude -->
</math>

Label

Another main attribute is label. This does the same as the label of a stream (see Streampool) and can be used to give the altered data to a generic (see Generics).

<math id="degrees" src="raw:gps" label="generic:coords">
  <op>i2=(i2-i2%100) + (i2%100)/60</op> <!-- latitude -->
</math>

Scale This attribute allows to have the result of an operation scaled to a certain amount of digits.

<op scale="3">i2=(i2-i2%100) + (i2%100)/60</op><!-- latitude 51.7077183 becomes 51.708-->

Cmd

This feature allows the result of an op to be used in a command. So if we wanted to store the earlier calculated latitude as an rtval, this could be done by adding the cmd attribute and the relevant command as value.
In such command, the $ will be replaced with the calculated value.

<!-- Some info on what the test math does -->
<math delimiter=";" id="test" src="raw:id:sensor">   
      <op cmd="doubles:new,latitude,$" >i2=(i2-i2%100) + (i2%100)/60</op> <!-- latitude -->
</math>

Note: See 'RealtimeValues' for explanation on the doubles command

Defined References

When using this to calibrate scientific values from a sensor outputting raw data, there are always calibration coefficients involved. Given that these tend to change overtime, it's possible to have mentioned separately instead of in the calculation.
Just make sure you don't use 'i' followed by a number.

<math id="convert">
	  <!-- Calibration values of the pt100 -->
	  <def ref="pt1_x0">-196,2332201</def>
	  <def ref="pt1_x1">9,7149867E-06</def>
	  <def ref="pt1_x2">2,4479084E-13</def>
	  
      <!-- Second order calculation -->
	  <op scale="3">i0=pt1_x2*i0^2 + pt1_x1*i0 + pt1_x0</op> <!-- Calculate second order, than scale to 3 fractional digits -->
</math>	

Suffix

This allows for certain (calculated) data to be appended to the final output.
For now the only one is nmea, which will add the NMEA checksum (including the *) at the end.

Some rules/tips for the operations

  • Spaces are allowed but not required
  • Every operation will be executed in order, if the first op writes to i1 the next op will use the updated value
  • Both . or , are valid decimal indicators
  • Brackets are not required if the order is followed, but might improve readability
  • If parts are repeated, they are only calculated once in a single operation
    fe. i1=(i1+1)+(i1+1) will result in i1+1 only executed once, see 'how it's implemented' for explanation

How it is implemented

Consider the data: 12,16,2,4,6
and the op: i1=((i1+8)*2+(i1+8))*(i2+i4)

Splitting the operation in subs follows these rules:

  • brackets from left to right (starting with first closed bracket)
  • order of operations
  • one operand at a time

Based on this, the first encountered sub is i1+8, this will be stored as a function and the result is written to o1. This simplifies the expression to (o1*2+o1)*(i2+i4).

Using this logic for the whole operation

Sub Function New expression
i1+8 o1=i1+8 i1=(o1*2+o1)*(i2+i4)
o1*2 o2=o1*2 i1=(o2+o1)*(i2+i4)
o2+o1 o3=o2+o1 i1=o3*(i2+i4)
i2+i4 o4=i2+i4 i1=o3*o4
o3*o4 o5=o3*o4 i1=o5

So the computations are:
o1 = 12 + 8 = 20
o2 = 20 * 2 = 40
o3 = 20 + 40 = 60
o4 = 16 + 4 = 20
o5 = 60 * 20 = 1200

So the operation required five computations to change the data to 1200;16;2;4;6

Clone this wiki locally