Data Route

Data routes provide a simple and compact way for developers to access MetaWear’s advanced features such as logging, data processing, and on-board event handling.

Creating Routes

Routes are created by calling the AddRouteAsync function of the IDataProducer you want to interact with. The AddRouteAsync function accepts an Action delegate with an IRouteComponent parameter. This parameter is used to define how data flows from a producer to different endpoints.

public async void CreateRouteExample(IDataProducer producer) {
    await producer.AddRouteAsync(source => {
        // define route here with IRouteComponent objects
    });
}

All route objects have a unique numerical value associated with them. You can retrieve routes anytime using this numerical ID with the LookupRoute method.

Handling Data

Data created by the data producers is represented by the IData interface, encapsulating key attributes such as the time the data was created (or receieved) and the value of the data sample. IData objects are consumed by the delegate attached to the route via the stream or log component.

When accessing the data value, you need to specify what type the value should be casted to. Valid types passed to the Value method differ depending on the data producer; an InvalidCastException is thrown if an invalid class type is used. Developers can use the Types property to get a list of valid types that to be used with the Value function.

public static void LogDataTypes(IData data) {
    Console.WriteLine("Types: " + String.Join<Type>(", ", data.Types));
}

Stream

Creating a live data stream to your Win10 device is handled with the Stream component. The data from the most recent producer will be sent live to the delegate.

public async void StreamData(IDataProducer producer) {
    await producer.AddRouteAsync(source => source.Stream(data => Console.WriteLine(data.ToString())));
}

Log

Alternatively, you may want to record data to the on-board flash memory and retrieve it at a later time. Constructing a logging route follows the same steps as a streaming route except you use the Log component.

public async void LogData(IDataProducer producer) {
    await producer.AddRouteAsync(source => source.Log(data => Console.WriteLine(data.ToString())));
}

Note that this only creates a route to handle logged data; you still need to tell the logger when to start/stop logging data and when to download the recorded data. More information on controlling the logger is provided in the Logging section.

Reaction

A reaction is a collection of MetaWear commands programmed onto the board that is executed when the source producer has created new data. Developers can use this feature to have the board react to new data without needing maintain an active connection to the board.

The MetaWear commands you want programmed onto the board are contained in an empty parameter Action delegate which is passed into the React component.

// Turn on the led everytime new data is available from the producer
public async void AddReaction(IDataProducer producer, ILed led) {
    await producer.AddRouteAsync(source => source.React(() => {
        led.SetPresetPattern(Color.Blue, PatternPreset.Solid, delay: 0);
        led.Play();
    }));
}

Split

Splitters break down combined data into its individual components i.e. the xyz values in acceleration data. When you add the Split component, you can refer to each data component with the Index component. Note that you must call Index immediately after calling Split.

public async void SplitAccData(IAccelerometer accelerometer) {
    await accelerometer.Acceleration.AddRouteAsync(source =>
        source.Split().Index(2).Stream(data => Console.WriteLine("z-axis: " + data.Value<float>()))
    );
}

Multicast

The Multicast component creates branches in the route where the same data can be pass to different route components. Starting a new branch is expressed with the To component and you can specify as many branches as you need provided the firmware has enough resources to allocate the additional route components. Keep in mind that you must call To immediately after calling Multicast.

using MbientLab.MetaWear.Sensor;
using MbientLab.MetaWear.Sensor.Temperature;

public async void CreateMulticast(ISensor tempSensor) {
    await tempSensor.AddRouteAsync(source =>
        source.Multicast()
            .To().Stream(data => Console.WriteLine("Celsius = " + data.Value<float>()))
            .To().Map(Function2.Multiply, 18).Map(Function2.Divide, 10).Map(Function2.Add, 32)
                .Stream(data => Console.WriteLine("Fahrenheit = " + data.Value<float>()))
            .To().Map(Function2.Add, 273.15f)
                .Stream(data => Console.WriteLine("Kelvin = " + data.Value<float>()))
    );
}

Data Processing

One of the neat features of the MetaWear firmware is the abiliy to manipulate data on-board before passing it to the user. Processors can be chained together to combine multiple operations in one route. Note that data processors can have Stream and Log components attached to them as well.

Data processors are identified by a globally unique name using the Name component. This name is used to identify the processor in the Data Processor module or to construct feedback loops with the comparator or mapper.

Account

The accounter processor adds additional information to the BTLE packet to reconstruct the data’s timestamp, typically used with streaming raw accelerometer, gyro, and magnetometer data. This processor is designed specifically for streaming, do not use with the logger.

public async void accountData(IAsyncDataProducer producer) {
    await producer.AddRouteAsync(source => source.Account().Stream(
        data => Console.WriteLine("realtime: " + data.Timestamp.ToString("yyyy-MM-ddTHH.mm.ss.fff"))
    ));
}

If there is not enough space to append timestamp data, i.e. sensor fusion outputs, a sample count can instead be added to the packet. The count value is accessed via the Extra function.

public async void accountData(IAsyncDataProducer producer) {
    await producer.AddRouteAsync(source => source.Account(AccountType.Count).Stream(
        data => Console.WriteLine("sample: " + data.Extra<uint>())
    ));
}

Accumulate

An accumlator tallies a running sum of all data that passes through. The running sum can be reset to 0 or set to a specific value using an IAccumulatorEditor.

// sum the values from gpio abs reference voltage
public async void AccumAbsRef(IPin pin) {
    await pin.AbsoluteReference.AddRouteAsync(source => source.Accumulate());
}

Average

This component is renamed to LowPass in SDK v0.2.

Buffer

Buffers store the most recent input in its internal state which can accessed using the State method from the Data Processor module. As there is no output from the Buffer processor, you cannot chain additional route components after the buffer.

public async void BufferTempData(ISensor tempSensor) {
    // store temp data in buffer named "temp_buffer"
    // read buffer state with DataProcessor module
    await tempSensor.AddRouteAsync(source => source.Buffer().Name("temp_buffer"));
}

Count

Add a Count component to tally the number of data samples received. The output from this processor is the current running count. Use a ICounterEditor to reset the count or set it to a specific value.

public async void CountData(IDataProducer producer) {
    await producer.AddRouteAsync(source => source.Count());
}

Delay

The Delay component stalls further route activity until it has collected N samples.

// Collect 16 data samples before letting it pass
public async void delayData(IAccelerometer accelerometer) {
    await accelerometer.Acceleration.AddRouteAsync(source =>
        source.Split().Index(2).delay((byte) 16);
    );
}

Filter

Filter processors remove data that do not satisfy a given condition and are added to a route using the filter component.

Comparator

The comparison Filter removes data from the route whose value does not satisfy the comparison operation. All 6 comparison operations (eq, neq, lt, lte, gt, gte) are supported.

using MbientLab.MetaWear.Builder;

public async void CompareTempData(ISensor tempSensor) {
    // removes temperature data that is not greater than 21C
    // from the route
    await tempSensor.AddRouteAsync(source => source.Filter(Comparison.Gt, 21f));
}

As of firmware v1.2.3, the comparator has been updated to compare against multiple values. The variant Filter component accepts an extra ComparisonOutput enum which provides other information about the multi-value comparison.

Output Descripion
Absolute Input value is returned when the comparison is satisfied, behavior of old comparator
Reference The reference value that satisified the comparison is outputted
Zone Outputs the index (0 to n-1) of the reference value that satisfied the comparison, n if none are valid
Pass / Fail 0 if the comparison fails, 1 if it passed
using MbientLab.MetaWear.Builder;

public async void MultiCompareTempData(ISensor tempSensor) {
    // Create 3 ranges: T < 0C [0], 0C < T < 21f [1], and 21C < T < 31C [2]
    // return which range the input resides in
    await tempSensor.AddRouteAsync(source =>
        source.Filter(Comparison.Lt, ComparisonOutput.Zone, 0f, 21f, 38f)
    );
}

Keep in mind that if you are using zone or pass/fail type comparisons, the comparison will be treated like a Map component instead. You will need to chain an additional absolute or reference type comparison to restore the original filter behavior.

public async void ZoneCompareFilter(ISensor tempSensor) {
    await tempSensor.AddRouteAsync(source =>
        source.Filter(Comparison.Lt, ComparisonOutput.Zone, 0f, 21f, 38f)
            // do not let (zone == 3) values through i.e. prior zone comparison failed
            .Filter(Comparison.Neq, ComparisonOutput.Absolute, 3)
    );
}

Find

The Find component scans the data to see if the collected data satisfies a pattern. While similar to a filter in that finders remove data from the stream, a finder maintains an internal state and analyzes the data as a whole rather than sample by sample.

Differential

Differential processors compute the distance between sequential values, and only outputs results when the distance is greater than a set threshold. When the processor outputs a value, the reference point will be updated to the last valid value.

This processor also has three output modes that provide different information about the input data:

Type Description
Absolute Input passed through as is
Differential Difference between current and reference
Binary 1 if current < reference, -1 if current > reference
using MbientLab.MetaWear.Builder;

public async void AdcDifferentialFilter(IPin pin) {
    // Remove ADC data that is not at least 128 steps
    // from the reference point
    // Output the difference between the reference and input values
    await pin.Adc.AddRouteAsync(source => source.filter(Differential.Difference, 128));
}

Threshold

The threshold processor checks if the data crosses a boundary value, whether rising above the boundary or falling below it. It also has an alternate output mode that reports which direction the boundary was crossed.

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

To prevent oscillations around the boundary from sending multiple data samples through, a hysteresis value can be set so that the threshold filter will only allow values that cross the boundary and lay outside the range [boundary - hysteresis, boundary + hysteresis].

using MbientLab.MetaWear.Builder;

public async void ThsAccXAxis(IAccelerometer accelerometer) {
    // let x-axis acceleration data through if it crosses the 1g boundary
    // with +/- 0.0001g of hysteresis i.e.
    // must be below 0.999g and above 1.0001g
    await accelerometer.Acceleration.AddRouteAsync(source =>
        source.Split().Index(0).Filter(Threshold.Binary, 1f, 0.001f)
    );
}

Pulse

A data pulse is defined as a minimum number of consecutive data points that rises above then falls below a threshold. Both the threshold and minimum sample size can be later modified using an IPulseEditor.

This processor also has 4 output modes that provide different contextual information about the pulse.

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
On Detect Return 0x1 as soon as pulse is detected
public async void FindAdcPulse(IPin pin) {
    // find a pulse that has a minimum of 16 samples
    // rise above then fall below  512
    // Output the max value of the pulse
    await pin.Adc.AddRouteAsync(source => source.Find(Pulse.Peak, 512, 16));
}

High Pass

High pass filters compute the difference of the current value from a running average of the previous N samples. Output from this processor is delayed until the first N samples have been received. Use the IHighPassEditor to reset the running average.

public void accHpfData(IAccelerometer accelerometer) {
    // delay stream by 4 samples, 5th sample and on are high pass filtered
    accelerometer.Acceleration.AddRouteAsync(source => source.HighPass(4).Stream(
        data => Console.WriteLine("hpf data = " + data.Value<Acceleration>())
    ));
}

Low Pass

Low pass filters compute a running average of the current and previous N samples. Output from this processor is delayed until the first N samples have been received. Use the ILowPassEditor to reset the running average.

public async void AverageAdc(IPin pin) {
    // compute running average over 4 ADC values
    await pin.Adc.AddRouteAsync(source => source.LowPass(4));
}

Limit

Limiters control the amount of data that flows through the route. Add them to a route using the Limit component.

Passthrough

The passthrough limiter functions as a user controlled gate using the value parameter to determine when to let data pass. There are three types of passthrough limiters:

Type Description
All Allows all data to pass
Conditional Only allow data through if value > 0
Count Only allow a fixed number of samples through

Both the value and type parameters can be modified using an IPassthroughEditor.

public async void DataPassthrough(IDataProducer producer) {
    // Only allow 16 data samples through
    // Use IDataProcessor module to reset the count when all 16 values pass
    await producer.AddRouteAsync(source =>
        source.limit(Passthrough.count, 16).name("acc_passthrough")
    );
}

Time

Time limiters reduce the frequency at which data flows through the route. They are typically used to stream data at frequencies not natively supported by the sensor, or combined with a data processing chain to only stream processed data at certain intervals.

// Reduce data rate to 10Hz
public async void LimitData(IDataProducer producer) {
    await producer.AddRouteAsync(source => source.Limit(100));
}

Map

A mapper applies a function to the data letting developers modify the value of each data sample. All basic arithemtic is supported along with some bit shifting, sqrt, vector magnitude and rms.

// Apply the RMS function to all acceleration data
public async void MapAccData(IAccelerometer accelerometer) {
    await accelerometer.Acceleration.AddRouteAsync(source =>
        source.map(Function1.Rms)
    );
}

Packer

The packer processor combines multiple data samples into 1 BLE packet to increase the data throughput. You can pack between 4 to 8 samples per packet depending on the data size.

Note that if you use the packer processor with raw motion data instead of using their packed data producer variants, you will only be able to combine 2 data samples into a packet instead of 3 samples however, you can chain an accounter processor to associate a timestamp with the packed data.

public async void packData(IDataProducer producer, ref int count) {
    await producer.AddRouteAsync(source => source.Pack(4).Stream(
        data => Console.WriteLine("samples: " + (count++))
    ));
}