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));
}
Fuser¶
The fuser processor combines data from multiple sensors into 1 message. When fusing multiple data sources, ensure that they are sampling at the same frequency, or at the very least, integer multiples of the fastest frequency. Data sources sampling at the lower frequencies will repeat the last received value.
To use the fuser, you first need to direct the other pieces of data to a named Buffer processor. Then, pass the processor names into the Fuse component.
public void FuseImuData(IAccelerometer acc, IGyroBmi160 gyro)
await gyro.AngularVelocity.AddRouteAsync(source => source.Buffer().Name("gyro-buffer"));
await acc.Acceleration.AddRouteAsync(source => source.Fuse("gyro-buffer").Stream(_ => {
var dataArray = _.Value<IData[]>();
// accelerometer is the source input, index 0
// gyro name is first input, index 1
Console.WriteLine("acc = {0}, gyro = {1}", dataArray[0].Value<Acceleration>(),
dataArray[1].Value<Acceleration>());
}));
}
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++))
));
}