Custom task functions

By ProFuN

The user of the ProFuN Task Graph is not limited by the software components available out-of-box. For one, he can write his own components (“nodes” in Node-RED terminology - don’t confuse them with hardware sensor nodes!). For another, he can use one of the two function components on the palette. Through these components, model-specific functionality can be implemented.

Functions can be programmed in C, but they can also be programmed in SEAL, a WSN-specific node-level programming language.

Table of Contents

C functions

For this tutorial, we are going to use a well-known data processing function: exponentially weighted moving average (EWMA).

Start by designing a simple task graph with a pair of communicating tasks:

The inject node sends out an infinitely-long stream of random numbers in range from 100 to 200.

When newly created, the function node is invalid by default. To make it valid, assign it a name. The name must be a valid C identifier (i.e. must consist only of alphanumeric symbols together with underscore _). So let’s call it “ewma”.

Now you should be able to compile the model and run in the simulator. That was easy!

Unfortunately, the function does nothing yet. The code itself still has to be written. The code of a “C function” (in task-graph terminology) actually consists of three different functions (in C language terminology):

  • initialization;
  • periodic actions;
  • input data item received.

The initialization function is executed when the task is created. The periodic function is executed only for tasks with no inputs; its frequency is determined by the “Period” configuration parameter. Finally, the input data processing function is executed when the task receives data from some other task, so it’s not required for functions with zero inputs configured.

EWMA function is going to have a single input and produce a single output. This means that periodic function for this task can be left empty.

Initialization function also could be left empty, but we are going to use it for:

  • #defined constants - model-specific parameters of the task;
  • printing an initialization message;
  • setting the initial value.
// start with some #defines
#define ALPHA         0.1
#define INITIAL_VALUE 0

// print something to serial port
puts("ewma initialization function");
// Zero out the value of the saved state.
// Note that this code is not actually required, as the parameter array
// is guaranteed to contain zero on initialization.
t->u.param32[0] = INITIAL_VALUE;

The variable t is of type struct task *. The task structure contains 8 byte “parameter” array. The area is used for configured task parameters (for example, upper and lower bound of the inject task), as well as state information. The array can be treated either as four 16-bit values, two 32-bit values, or a single 64-bit integer. Here we’re using it as 32-bit value. (A more precise EWMA implementation would use it as floating point or double number - it can be manually cast to that).

The real job is done in the handle_input function:

uint32_t new_value;
// calculate the new value
new_value = ALPHA * msg->value + (1 - ALPHA) * t->u.param32[0];
// print the value to serial
printf("ewma: %ld\n", new_value);
// store the calculated value for future
t->u.param32[0] = new_value;
// output the calculated value
profun_produce(t, OUTPUT_ID_DEFAULT, new_value);
// return true - we have produced an output
return true;

This is a fairly straightforward EWMA implementation that uses floating point multiplication. Integer-only implementation could be used for extra speed, but that would be more complex (and, likely, less precise).

The newly calculated value is both saved in the task’s state (the parameters), and sent out, by invoking the function profun_produce.

Save the model, allocate the tasks, and run the result in simulator. The idea is that after a sufficient number of samples, the output value should converge to the interval 145..155, even when starting from zero.

That may require a couple of minutes, but eventually the EWMA function should be producing data in the desired range.

SEAL functions

A quick intro to SEAL

SEAL is a small, declarative, and novice-friendly sensor node programming language. It is primarily targeted towards node-local functionality (sense, process data, and actuate), although it also features components for network access.

SEAL makes very easy to write periodical functionality. For example, Blink (the “hello world” of embedded programming) can be written in a single line:

use Led;

Or, alternatively, with a period different from the blinking (on/off) default period of one second:

use Led, period 200ms;

Event detection is also pretty easy:

input TaskGraph (myInputData);
when myInputData > 1:
    use Print, format "received myInputData greater than one: %ld", arg1 myInputData;
end

Sequential execution can achieved by using do/then blocks:

do:
    use RedLed, on;      // turn on the red LED
then, initialTimeout 1s: // wait one second
    use BlueLed, on;     // turn on the blue LED
end

More information is available in SEAL documentation and publications: the paper, poster abstract, and demo abstract

EWMA with SEAL

Delete the C function from the model, and replace it with a SEAL function node. Open SEAL function’s configuration set a name to make it valid.

SEAL provides a number of built-in data processing functions; they include EWMA, a function for exponentially weighted moving average. In SEAL, EWMA is a two-argument function; the first argument is a name of a scalar-value stream (such as a sensor or an input), and the second argument must be a constant corresponding to alpha.

The EWMA example rewritten in SEAL is much less complex; most of the code is simple boilerplate (input/output trivia):

// define alpha value
const ALPHA 0.1;
// read our input value
input TaskGraph(data);
// calculate the new value
define result ewma(data, ALPHA);
read result;
// print the new value
output Serial(result);
// output the new value
output TaskGraph(result);

The important lines are:

define result ewma(data, ALPHA);
read result;

The first line defines a virtual sensor called result. The value of this sensor is defined to be the value of the built-in function ewma with parameter ALPHA applied to the input data (Alternatively, the name could be spelled EWMA - SEAL syntax is not case sensitive.)

The output to serial port (i.e. printing the value) is not required for the functionality and could be removed. However, it allows to see the results printed in the simulator:

Compared with the C approach to the same problem, SEAL code has a number of benefits:

  • it is not partitioned in three distinct functions;
  • it does not require much of knowledge about the executing environment, and the task structure;
  • it does not require reinterpreting the parameters array or other low-level hacks;
  • it does not use file-scoped #defines;
  • advanced functionality (e.g. periodic self-scheduling of the task) is almost trivial to write down;
  • it cannot cause the whole system to misbehave or even reboot because of an accidental infinite loop in the code: it is impossible to write non-interruptible loops in SEAL.

Download

The resulting model files are available: