Data Processor

Data processors are functions that let you maniplate sensor data on-board. They can be categorized into two types: filters and transformers. Filters function like a gate, only allownig data through that satisfies some criteria; they do not modify the input data in any way. On the other hand, transformers manipulate the data, altering the data to enable it to be used differently from its default format. By chaining data processors together, you can turn your board into something much more than a simple data collector, as demonstrated in the Fitness Tracker app.

Some processors have an internal state that can be modified by the user to alter its behaviour. When modifying a processor’s internal state it is important that the State subclass matches the processor type i.e. an accumulator processor is modified with Accumulator.State and Accumulator objects. Most processors can have their initial configuration modified as well, however, some configuration settings cannot be changed once a route has been committed. Please check that the correponsding processor sub-section for further details regarding configuration editing.

Processors are configured with a subclass of the ProcessorConfig interface. These subclasses correspond to the available processors and allows the IDE to perform some static checks during compile time. You can alternatively use a URI string to express the processor configuration in a more compact form. The URI scheme is explained in more detail on the project’s GitHub wiki.

Accumulator

An accumulator transformer tallies a running sum over all data received. An optional “output” parameter can be passed in to explicitly set the output size. If the “output” parameter is ignored, the API will infer an appropriate size.

// Allocate 4 bytes to accumulate data
.process(new Accumulator((byte) 4));

// Accumulate data, infer output size based on input signal
.process(new Accumulator());

Once added, the accumulator’s configuration cannot be changed. Users can modify the processor’s internal state, setting the new accumulated sum.

Temperature tempModule= mwBoard.getModule(Temperature.class);

tempModule.routeData().fromSensor()
    .process("accum_proc", new Accumulator())
    .monitor(new ActivityHandler() {
        @Override
        public void onSignalActive(Map<String, DataProcessor> processors,
                DataToken token) {
            // Set the running sum to 100C
            processors.get("accum_proc").setState(new Accumulator.State(100));
        }
    });
.commit();

Average

An average transformer computes a running average over all data received. Users can set the sample size of the average computation and it is recommended that it be set to a power of 2 to speed up the average computation.

// Compute the running average over 16 data samples
.process(new Average((byte) 16));

The sample size can be later changed by modifying the processor configuration. Users can also reset the running average by setting the internal state.

Temperature tempModule= mwBoard.getModule(Temperature.class);

tempModule.routeData().fromSensor()
    .process("avg_proc", new Average((byte) 8))
    .monitor(new ActivityHandler() {
        @Override
        public void onSignalActive(Map<String, DataProcessor> processors,
                DataToken token) {
            // resets the running average to 0
            processors.get("avg_proc").setState(new Average.State());
        }
    });
.commit();

Buffer

A buffer filter that captures input data which can be later retrieved by reading the data processor state using the DataProcessorModule class. This filter does not output any data so it is meaningless to continue the route after a buffer filter. The buffer filter’s state will be updated when new data is received.

The buffer filter can be used to store data that is produced asynchronously. For example, if you want to periodically check the value of a sensor but the sensor produces data asynchronously, you can store the sensor data into a buffer filter and use a timer to read the buffer state.

final String procKey= "buffer", streamKey= "buffer_state_stream";
final Switch switchModule = mwBoard.getModule(Switch.class);
final DataProcessorModule dpModule= mwBoard.getModule(DataProcessorModule.class);
final Timer timerModule = mwBoard.getModule(Timer.class);

// The route counts the number of switch actions (pressed/released)
// and stores that value into the buffer filter.
switchModule.routeData().fromSensor()
    .process(new Counter())
    .process(procKey, new Buffer())
.commit().onComplete(new CompletionHandler<RouteManager>() {
    @Override
    public void success(RouteManager result) {
        // Create a route for the data from the processor state
        dpModule.routeData().fromState(procKey, false).stream(streamKey).commit()
            .onComplete(new CompletionHandler<RouteManager>() {
                @Override
                public void success(RouteManager result) {
                    result.subscribe(streamKey, new RouteManager.MessageHandler() {
                        @Override
                        public void process(Message msg) {
                            Log.i("MainActivity", "Buffer State: " + msg.getData(Integer.class));
                        }
                    });
                }
            });

        // Read the buffer filter every 1000 milliseconds
        timerModule.scheduleTask(new Timer.Task() {
            @Override
            public void commands() {
                dpModule.readState(procKey, false);
            }
        }, 1000, false).onComplete(new CompletionHandler<Timer.Controller>() {
            @Override
            public void success(Timer.Controller result) {
                result.start();
            }
        });
    }
});

Comparison

A comparison filter only allows data through that satisfies the comparison operation. Users can optionally force a signed or unsigned comparison, or let the API decide the comparison type. This processor also supports using a DataToken in lieu of setting a fixed reference value.

Temperature tempModule= mwBoard.getModule(Temperature.class);

tempModule.routeData().fromSensor()
    // Only allow temp values through that are greater than or equal to 37C
    .process(new Comparison(Comparison.Operation.GTE, 37))
.commit();

There is no internal state to modify for this processor though users can modify the configuration to change the operation and reference value.

Temperature tempModule= mwBoard.getModule(Temperature.class);

tempModule.routeData().fromSensor()
    // Initially, only allow temp values through that are greater than 37C
    .process("comp_filter", new Comparison(Comparison.Operation.GT, 37))
    .monitor(new ActivityHandler() {
        @Override
        public void onSignalActive(Map<String, DataProcessor> processors,
                DataToken token) {
            // If a value greater than the reference value is received,
            // update the comparison to use the new, higher value
            processors.get("comp_filter").modifyConfiguration(new Comparison(
                    Comparison.Operation.GTE, token));
        }
    })
.commit();

Multi-Value Comparison

Starting from firmware v1.2.3, the comparison filter can accept multiple reference values to compare against and has additional operation modes that configure the output values and when output are produced. The multi-value comparison filter is an extension of the comparison filter implemented on older firmware.

The multi-value comparator operation modes are listed in the Comparison.Mode enum and the entries are documented below:

Mode Processor Operation
Absolute Input value is outputted when the comparison is satisfied
Reference The reference value is output when the comparison is satisfied
Zone Outputs the index (0 based) of the reference value that satisfied the comparison, n if none are valid
Pass / Fail 0 if the comparison fails, 1 if it passed

Also note that you can only use one reference value when creating feedback/feedforward loops.

mwBoard.getModule(Gpio.class).routeData()
    .fromAnalogIn((byte) 0, Gpio.AnalogReadMode.ADC)
    // Return 0 - 4 for each comparison where 4 means no references satisfy the GTE comparison
    .process(new Comparison(Comparison.Operation.GTE, Comparison.Mode.ZONE, 128, 256, 512, 1024))

Counter

The counter transformer counts the number of data samples that have passed through.

Gpio gpioModule= mwBoard.getModule(Gpio.class);

gpioModule.routeData().fromDigitalIn((byte) 0)
    // Count how many digital input samples have passed through
    .process("di_sample_counter", new Counter())
.commit();

Its state can be modified to set the number of samples received.

Switch switchModule= mwBoard.getModule(Switch.class);

switchModule.routeData().fromSensor().monitor(new ActivityMonitor() {
    @Override
    public void onSignalActive(Map<String, DataProcessor> processors, DataToken token) {
        // Reset the sample counter to 0 when the button is pressed
        processors.get("di_sample_counter").setState(new Counter.State(0));
    }
})
.commit();

Delta

The delta processor is both a transformer and filter. It only allows data through whose distance from a reference value exceeds a threshold and has three output modes with two that transforms the data.

Output Transformation Data Type ID
Absolute Input passed through untouched Same as input source i.e. float -> float
Differential Difference between current and previous If input is unsigned int, output is signed int
Binary 1 if difference > 0, -1 if less than 0 Output is always signed int

The reference value will continually update to the value that satisfies the distance condition.

mwBoard.getModule(Temperature.class).routeData().fromSensor()
    .split()
        // Let data through that has an offset of 2C, leave input as is
        .branch().process("temp_delta_abs", new Delta(Delta.OutputMode.ABSOLUTE, 2))
        // Let data through that has an offset of 4C, output the difference
        // from the reference value
        .branch().process(new Delta(Delta.OutputMode.DIFFERENTIAL, 4))
    .end()
.commit();

mwBoard.getModule(Gpio.class).routeData().fromAnalogGpio((byte) 0, Gpio.AnalogReadMode.ADC)
    // Let data through that has an offset of 127, output if the value
    // was greater or less than the reference
    .process(new Delta(Delta.OutputMode.BINARY, 127)).monitor(new ActivityHandler() {
        @Override
        public void onSignalActive(Map<String, DataProcessor> proceesors, DataToken token) {
            // Set the previous reference value to 512
            processors.get("temp_delta_abs").setState(new Delta.State(512));
        }
    })
.commit();

Users can modify the processor state to set the reference value and modify the configuration to change the threshold.

Maths

The maths processor performs simple arithmetic or logical operations on the input. Users can force a signed or unsigned operation, or let the API decide the appropriate comparison type. The processor configuration supports using a DataToken in lieu of using a fixed right hand value.

// Converts temp data from C to F
mwBoard.getModule(Temperature.class).routeData().fromSensor()
    .process(new Maths(Maths.Operation.Multiply, 18))
    .process(new Maths(Maths.Operation.DIVIDE, 10))
    .process(new Maths(Maths.Operation.ADD, 32.f))
.commit();

The processor does not have any internal state though users can update the configuration to change the right hand value.

mwBoard.getModule(Gpio.class).routeData().fromAnalogGpio((byte) 0, Gpio.AnalogReadMode.ADC)
    .process("analog_in_maths", new Maths(Maths.Operation.MULTIPLY, 0))
.commit();

mwBoard.getModule(Switch.class).routeData().fromSensor()
    .monitor(new ActivityHandler() {
        @Override
        public void onSignalActive(Map<String, DataProcessor> processors, DataToken token) {
            // Use the switch data (1 or 0) as the second operation.  This programs
            // the switch to function as a filter, only allowing data through
            // when pressed
            processors.get("analog_in_maths").modifyConfiguration(
                    new Maths(Maths.Operation.MULTIPLY, token));
        }
    })
.commit();

Passthrough

The passthrough procesor is a filter that allows data through depending on an internal value. It has three operation modes which affect how this value is used:

Mode Description
All Allow all data through
Conditional Only allow data through if the count > 0
Count Only allow a set number of samples through

This filter is typically used in an ActivityHandler as part of a feedback loop.

mwBoard.getModule(Temperature.class).routeData().fromSensor()
    // Only allow 8 temperature samples through, then block all data
    .process("temp_count_gate", new Passthrough(Passthrough.Mode.COUNT, (short) 8)
.commit();

mwBoard.getModule(Switch.class).routeData().fromSensor()
    .monitor(new ActivityHandler() {
        @Override
        public void onSignalActive(Map<String, DataProcessor> processors,  DataToken token) {
            // When the button is pressed or released, allow 8 more samples through
            processors.get("temp_count_gate").setState(
                    new Passthrough.State((short) 8));
        }
    })
.commit();

Users can modify the processor value and modify the configuration to change the operation mode.

mwBoard.getModule(Temperature.class).routeData().fromSensor()
    // Allow all temperate data through for now
    .process("temp_cond_gate", new Passthrough(Passthrough.Mode.CONDITIONAL, (short) 1)
.commit();

mwBoard.getModule(Switch.class).routeData().fromSensor()
    .monitor(new ActivityHandler() {
        @Override
        public void onSignalActive(Map<String, DataProcessor> processors, DataToken token) {
            // When the button is pressed or released, block all temperature data
            processors.get("temp_cond_gate").setState(
                    new Passthrough.State((short) 0));
        }
    })
.commit();

Pulse

The pulse processor detects and quantifies a pulse over a set of data samples. It can quantify a pulse in three ways:

Output Description
Width Number of samples that made up the pulse
Area Summation of all the data in the pulse
Peak Highest value in the pulse
mwBoard.getModule(Accelerometer.class).routeData().fromZAxis()
    // Check for a pulse that is over 1.0g on the accelerometer's z-axis.
    // The pulse detection will ues 16 samples and output the pulse's peak value
    .process(new Pulse(Pulse.OutputMode.PEAK, 1, 16))
.commit();

The processor does not have any internal state, though users can modify the configuration to change the threshold and pulse width.

Rms

The rms processor is a transformer that computes the root mean square over multi-component sensor data i.e. XYZ axis accelerometer measurements. This processor can only be attached to multi-component data sources such as accelerometer or gyro axes data.

This processor does not have any internal state nor configuration to change.

mwBoard.getModule(Accelerometer.class).routeData().fromAxes()
    // Compute root mean square over the XYZ axis data
    .process(new Rms())
.commit();

Rss

The rss processor is a transformer that computers the root sum square (vector magnitude) over multi-component sensor data i.e. XYZ axis measurements from the accelerometer. This processor can only be attached to multi-component data sources such as accelerometer or gyro axes data.

This processor does not have any internal state nor configuration to change.

mwBoard.getModule(Accelerometer.class).routeData().fromAxes()
    // Compute vector magnitude over the XYZ axis data
    .process(new Rss())
.commit();

Sample

The sample processor acts like a bucket, only allowing data through once the bin has collected N sample’s. There is no internal state to modify though users can modify the configuration to change the bin size.

mwBoard.getModule(Accelerometer.class).routeData().fromXAxis()
    // Only allow data through once 16 samples have been collected
    .process(new Sample((byte) 16))
.commit();

Threshold

The threshold processor is both a filter and transformer. It only allows data through that crosses a boundary and has two output modes that alter the input data:

Output Transformation
Absolute Input passed through untouched
Binary 1 if value rose above, -1 if it fell below

There is no internal state to set though users can modify the configuration to change the threshold boundary and hysteresis.

mwBoard.getModule(Temperature.class).routeData().fromSensor()
    .split()
        // Let data through that crosses the 30C boundary, leave input as is
        .branch().process(new Threshold(Threshold.OutputMode.ABSOLUTE, 30))
        // Let data through that crosses the 25C boundary, output the difference
        // from the boundary
        .branch().process(new Threshold(Threshold.OutputMode.DIFFERENTIAL, 25))
        // Let data through that crosses the 20C boundary, output 1 if value was
        // greater than the boundary, -1 if less than
        .branch().process(new Threshold(Threshold.OutputMode.BINARY, 20))
    .end()
.commit();

Time

The time processor is both a filter and transformer, periodically allowing data through. In differential mode, the processor converts the input data into a difference between it and the previous value.

There is no internal state to modify though users can modify the configuration to change the period.

mwBoard.getModule(Gpio.class).routeData().fromAnalogGpio((byte) 0,
        Gpio.AnalogReadMode.ADC)
    .process(new Accumulator())
    .split()
        // Let data through every 30 seconds as in
        .branch().process(new Time(Time.OutputMode.ABSOLUTE, 30000))
        // Let data through that crosses the 25C boundary, output the difference
        // between the current and previous value
        .branch().process(new Time(Time.OutputMode.DIFFERENTIAL, 20000))
    .end()
.commit();