.. highlight:: csharp 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(", ", 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 :doc:`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())) ); } 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())) .To().Map(Function2.Multiply, 18).Map(Function2.Divide, 10).Map(Function2.Add, 32) .Stream(data => Console.WriteLine("Fahrenheit = " + data.Value())) .To().Map(Function2.Add, 273.15f) .Stream(data => Console.WriteLine("Kelvin = " + data.Value())) ); } 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 :doc:`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()) )); } 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 :doc:`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 :ref:`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 :ref:`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(); // accelerometer is the source input, index 0 // gyro name is first input, index 1 Console.WriteLine("acc = {0}, gyro = {1}", dataArray[0].Value(), dataArray[1].Value()); })); } 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()) )); } 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++)) )); }