3. C#

The C# SDK can be used to create both Universal Windows Platform and .NET Core applications on Windows 10. Depending on which type of application you are building, the setup steps will be slightly different.

This tutorial will cover how to use the the SDK with .NET Core console applications.

3.1. Setup

Note

A finished C# project with all of the example code is located on the MetaWear C# Tutorial repo

Make sure you have completed the Windows Installation setup tutorial before proceeding with the rest of this tutorial. You will also need to install the .NET Core toolset from the Workloads page.

_images/netcore-windows-01.png

Double check that you have properly installed .NET Core by printing the installed version:

>>> dotnet --version

After you have confirmed that dotnet is properly installed:

  1. Create a new console application called NetCoreExamples
>>> dotnet new console -n NetCoreExamples
>>> cd NetCoreExamples
  1. Add a dependency to MetaWear.CSharp and MetaWear.CSharp.NetStandard.
>>> dotnet add package MetaWear.CSharp
>>> dotnet add package MetaWear.CSharp.NetStandard
  1. Open Program.cs and replace the contents with:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class Program {
        static void Main(string[] args) {
            MainAsync(args).Wait();
        }

        private static async Task MainAsync(string[] args) {
            var type = Type.GetType(args[0]);
            await (Task)type.GetMethod("RunAsync", BindingFlags.NonPublic | BindingFlags.Static)
                .Invoke(null, new object[] { args.TakeLast(args.Length - 1).ToArray() });
        }
    }
}

After adding new code blocks to the project, build the project and fix any reported errors before continuing.

>>> dotnet build

3.2. Scan and Connect

Note

Review the IMetaWearBoard documentation and Warble.NET’s LeScan example

Before diving too far into the MetaWear C# SDK, lets first scan for your device, then connect to it to retrieve some device information.

We will put the code for this tutorial in the ScanConnect class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using MbientLab.Warble;
using MbientLab.MetaWear;
using MbientLab.MetaWear.NetStandard;

namespace NetCoreExamples {
    class ScanConnect {
        static string ScanForMetaWear() {
            throw new NotImplementedException();
        }

        internal static async Task<IMetaWearBoard> Connect(string mac, int retries = 2) {
            throw new NotImplementedException();
        }

        static async Task RunAsync(string[] args) {
            var metawear = await Connect(ScanForMetaWear());
            Console.WriteLine($"Device information: {await metawear.ReadDeviceInformationAsync()}");
            await metawear.DisconnectAsync();
        }
    }
}

To scan for available MbientLab devices, we will use Warble.NET’s Scanner class.

The first thing to do is attach a handler to process the scan results. After setting the handler, start the BLE scan and wait until the user presses [Enter], which signals to stop the BLE scan. After stopping the scan, the user then selects which device to use or -1 to repeat the scan.

The ScanForMetaWear function will now looks as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static string ScanForMetaWear() {
    var devices = new List<ScanResult>();
    var seen = new HashSet<string>();
    // Set a handler to process scanned devices
    Scanner.OnResultReceived = item => {
        // Filter devices that do not advertise the MetaWear service or have already been seen
        if (item.HasServiceUuid(Constants.METAWEAR_GATT_SERVICE.ToString()) && !seen.Contains(item.Mac)) {
            seen.Add(item.Mac);

            Console.WriteLine($"[{devices.Count}] = {item.Mac} ({item.Name})");
            devices.Add(item);
        }
    };

    int selection;
    do {
        seen.Clear();
        devices.Clear();

        Console.WriteLine("Scanning for devices...");
        Scanner.Start();

        Console.WriteLine("Press [Enter] to stop the scan");
        Console.ReadLine();
        Scanner.Stop();

        Console.Write("Select your device (-1 to rescan): ");
        selection = int.Parse(Console.ReadLine());
        // Repeat until user selects a device
    } while (selection == -1);

    // return selected mac address
    return devices[selection].Mac;
}

Now that we have our device’s MAC address, we can retrieve an IMetaWearBoard object for our device from then .NET Standard SDK plugin, then use that to connect to the board.

We will also implement some retry logic so the code will attempt to connect a few times before throwing an exception.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
internal static async Task<IMetaWearBoard> Connect(string mac, int retries = 2) {
    var metawear = Application.GetMetaWearBoard(mac);

    Console.WriteLine($"Connecting to {metawear.MacAddress}...");
    do {
        try {
            await metawear.InitializeAsync();
            retries = -1;
        } catch (Exception e) {
            Console.WriteLine($"Error connecting to {metawear.MacAddress}, retrying...");
            Console.WriteLine(e);
            retries--;
        }
    } while (retries > 0);

    if (retries == 0) {
        throw new ApplicationException($"Failed to connect to {metawear.MacAddress} after {retries + 1} attempts");
    } else {
        Console.WriteLine($"Connected to {metawear.MacAddress}");
        return metawear;
    }
}

With all the helper functions implemented, run the scan and connect code. You should see a list of detected MetaMotion devices in the console, then the device information of the selected device.

>>> dotnet run -- NetCoreExamples.ScanConnect

3.3. LED

Note

Take a look at the LED developer documentation before starting this tutorial

MetaMotion boards come with all sorts of peripherals, such as an RGB LED. Each color (red, green, blue) can be programmed with different pulse characteristics, however, for this tutorial, we will use the preset Solid pattern.

To turn on a LED, you first set a pattern on one of the color channels, then play the pattern. When you want to turn off the LED, issue the stop command. The stop command can also clear the pattern if desired.

Lets create a Led.RunAsync function that demonstrates these concepts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Threading.Tasks;
using MbientLab.MetaWear.Core;
using MbientLab.MetaWear.Peripheral;
using MbientLab.MetaWear.Peripheral.Led;

namespace NetCoreExamples {
    class Led {
        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);

            var led = metawear.GetModule<ILed>();
            led.EditPattern(Color.Green, Pattern.Solid);
            led.Play();

            await Task.Delay(5000);
            led.Stop(true);

            // Have the board terminate the BLE connection
            // Ensures all commands are received
            await metawear.GetModule<IDebug>().DisconnectAsync();
        }
    }
}

Lines 12 and 13 set the green LED with the Solid pattern then play the programmed pattern, respectively, and line 16 issues a stop and clear command, simultaneously turning off the LED and clearing all programmed patterns.

Led.RunAsync lets the user pass in the MAC address directly to the C# SDK so you will need to specify it on the commandline. In this case, my board’s MAC address is E8:C9:8F:52:7B:07.

>>> dotnet run -- NetCoreExamples.Led E8:C9:8F:52:7B:07

3.4. Stream Temperature

While playing with LEDs can be quite entertaining, the main purpose of the MetaMotion boards is to provide sensor data. As an introduction to using the sensors, we will first look at the on-board thermistor, which measures ambient temperature.

Lets start by adding the following functions to the StreamTemperature class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using MbientLab.MetaWear;
using MbientLab.MetaWear.Core;
using MbientLab.MetaWear.Sensor;
using MbientLab.MetaWear.Sensor.Temperature;
using System;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class StreamTemperature {
        static async Task<IScheduledTask> Setup(IMetaWearBoard metawear) {
            throw new NotImplementedException();
        }

        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);

            Console.WriteLine($"Configuring {args[0]}...");
            var task = await Setup(metawear);
            // Start the periodic task
            task.Start();

            Console.WriteLine("Streaming data for 15s");
            await Task.Delay(15000);

            // Remove the task
            task.Remove();

            await metawear.GetModule<IDebug>().DisconnectAsync();
        }
    }
}

Temperature sensors are classified as forced data producers meaning they do not send data unless asked to. In order periodically sample data from these sensors, schedule periodic calls to the Read function with ScheduleAsync.

To get process data from data producers, we will setup a data route that tells the sensor to stream data to our host device using the stream component.

Lets implement the Setup function to create the data route and schedule the read task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static async Task<IScheduledTask> Setup(IMetaWearBoard metawear) {
    var temperature = metawear.GetModule<ITemperature>();
    // Use the on-board thermistor
    var thermistor = temperature.FindSensors(SensorType.PresetThermistor)[0];

    await thermistor.AddRouteAsync(source => source.Stream(_ => {
        Console.WriteLine($"{_.FormattedTimestamp} -> {_.Value<float>()}");
    }));
    // Schedule periodic `Read` calls every 1s in the firmware, do it after route is setup
    return await metawear.ScheduleAsync(1000, false, () => thermistor.Read());
}
>>> dotnet run -- NetCoreExamples.StreamTemperature E8:C9:8F:52:7B:07

3.5. Stream Accelerometer

Another sensor that MetaMotion boards come with is the accelerometer. This sensor is an async data producer, which sends data whenever new values are measured. You can control when the producer starts and stops sending data with IAsyncDataProducer.Start and IAsyncDataProducer.Stop respectively.

Start with the following skeleton code in the StreamAccelerometer class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using MbientLab.MetaWear;
using MbientLab.MetaWear.Core;
using MbientLab.MetaWear.Data;
using MbientLab.MetaWear.Sensor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class StreamAccelerometer {
        internal static async Task<IMetaWearBoard> PrepareBleConn(string mac) {
            throw new NotImplementedException();
        }

        internal static async Task Setup(IMetaWearBoard metawear, Dictionary<IMetaWearBoard, uint> samples) {
            throw new NotImplementedException();
        }

        internal static void StartStream(IMetaWearBoard metawear) {
            throw new NotImplementedException();
        }

        internal static Task StopStream(IMetaWearBoard metawear) {
            throw new NotImplementedException();
        }

        static async Task RunAsync(string[] args) {
            var metawear = await PrepareBleConn(args[0]);

            var samples = new Dictionary<IMetaWearBoard, uint> {
                { metawear, 0 }
            };

            // Connect and prepare the BLE connection
            await Setup(metawear, samples);
            Start(metawear);

            // Stream for 30
            await Task.Delay(30000);

            // Stop accelerometer and disconnect from the board
            await Stop(metawear);

            foreach (var (k, v) in samples) {
                Console.WriteLine($"{k.MacAddress} -> {v}");
            }
        }
    }
}

As usual, the first thing to do is to get an IMetaWearBoard object then connect to the board. After connecting to the board, we need to reduce the max BLE connection interval to 7.5ms so the BLE connection can handle the acceleroemter’s sampling frequency.

1
2
3
4
5
6
7
8
9
internal static async Task<IMetaWearBoard> ConnectToDevice(string mac) {
    var m = await ScanConnect.Connect(mac);

    m.GetModule<ISettings>()?.EditBleConnParams(maxConnInterval: 7.5f);
    await Task.Delay(1500);

    Console.WriteLine($"Connected to {mac}");
    return m;
}

The Setup function will configure the accelerometer to sample data at 100Hz with a range of +/-16g, and setup a data route to stream the data to the host device. When data is received, the handler will write it to the console along with the device it was received from.

1
2
3
4
5
6
7
8
9
internal static async Task Setup(IMetaWearBoard metawear, Dictionary<IMetaWearBoard, uint> samples) {
    var acc = metawear.GetModule<IAccelerometer>();

    acc.Configure(odr: 100f, range: 16f);
    await acc.Acceleration.AddRouteAsync(source => source.Stream(data => {
        Console.WriteLine($"{metawear.MacAddress} -> {data.Value<Acceleration>()}");
        samples[metawear]++;
    }));
}

With all of the setup done, we can start the acceleration data producer.

1
2
3
4
5
internal static void StartStream(IMetaWearBoard metawear) {
    var acc = metawear.GetModule<IAccelerometer>();
    acc.Acceleration.Start();
    acc.Start();
}

After we are done collection data, we will stop the accelerometer, then disconnect from the device. As with the LED example, the board terminates the BLE connection rather than the host device so ensure all commands are received.

1
2
3
4
5
6
7
8
internal static Task Stop(IMetaWearBoard metawear) {
    var acc = metawear.GetModule<IAccelerometer>();
    acc.Stop();
    acc.Acceleration.Stop();

    // Have remote device close the connection
    return metawear.GetModule<IDebug>().DisconnectAsync();
}

Now that all of the functions are implemented, run and code and move the board around to see the Acceleration change with the motions.

>>> dotnet run -- NetCoreExamples.StreamAccelerometer E8:C9:8F:52:7B:07

3.5.1. Stream Multiple Accelerometers

The previous section showed how to stream accelerometer data. By wrapping the various function calls in for loops, we can connect to more than 1 device and stream from multiple accelerometers simultaneously.

Create the StreamMultiAccelerometer class with this modified RunAsync code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using MbientLab.MetaWear;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class StreamMultiAccelerometer {
        static async Task RunAsync(string[] args) {
            var metawears = new List<IMetaWearBoard>();
            var samples = new Dictionary<IMetaWearBoard, uint>();

            // Connect and prepare the BLE connection for every device specified on the command line
            foreach (var _ in args) {
                var m = await StreamAccelerometer.PrepareBleConn(_);

                metawears.Add(m);
                samples.Add(m, 0);

                await StreamAccelerometer.Setup(m, samples);
            }

            foreach (var _ in metawears) {
                StreamAccelerometer.Start(_);
            }

            // Stream for 30
            await Task.Delay(30000);

            await Task.WhenAll(metawears.Select(_ => Stop(_)));

            foreach (var (k, v) in samples) {
                Console.WriteLine($"{k.MacAddress} -> {v}");
            }
        }
    }
}

The code flow is the same as in StreamAccelerometer.RunAsync with for loops to run the functions for each board.

After you feel comfortable with the code changes run the project again, this time passing in up to 3 MAC addresses. If you want to use more devices, reduce the sampling frequency and/or use the PackedAcceleration data producer.

>>> dotnet run -- NetCoreExamples.StreamMultiAccelerometer E8:C9:8F:52:7B:07 CB:B7:49:BF:27:33

3.6. Log Accelerometer

When collecting data, there will be times when you will not be able to maintain a connection to your host device e.g. sampling at high frequencies or in a noisy environment. For this purpose, MetaMotion comes equipped with additional flash memory that can store the sensor data, then send it to you when requested.

Create the LogAccelerometer class with the following skeleton code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using MbientLab.MetaWear;
using MbientLab.MetaWear.Core;
using MbientLab.MetaWear.Data;
using MbientLab.MetaWear.Sensor;
using System;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class LogAccelerometer {
        static async Task SetupLogger(IMetaWearBoard metawear) {
            throw new NotImplementedException();
        }

        static async Task DownloadData(IMetaWearBoard metawear) {
            throw new NotImplementedException();
        }

        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);

            Console.WriteLine($"Configuring {metawear.MacAddress}...");
            await SetupLogger(metawear);

            Console.WriteLine("Logging data for 15s");
            await Task.Delay(15000);

            Console.WriteLine("Downloading data");
            await DownloadData(metawear);

            Console.WriteLine("Resetting device...");
            await metawear.GetModule<IDebug>().ResetAsync();
        }
    }
}

For the logging tutorial, the code is split into two parts. First, it will configure the logger to log data from the accelerometer, then it will request the logged data after 15s.

Configuring the logger is very simple. Like when streaming the data, you also create a data route, except this time, you instead use the log component. You will also need to make a call to ILogging.Start before starting the accelerometer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static async Task SetupLogger(IMetaWearBoard metawear) {
    var acc = metawear.GetModule<IAccelerometer>();
    await acc.Acceleration.AddRouteAsync(source => source.Log(_ => {
        Console.WriteLine($"{_.FormattedTimestamp} -> {_.Value<Acceleration>()}");
    }));

    // Tell firmware to start logging
    metawear.GetModule<ILogging>().Start();
    acc.Acceleration.Start();
    acc.Start();
}

When you are ready to retrieve the logged data, stop the sensors and the logger, then call ILogging.DownloadAsycc. There are variants of this function that pass errors and progress updates to the caller.

Note

Before starting the download, set the max BLE connection interval to 7.5ms to get the maximum throughput

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static async Task DownloadData(IMetaWearBoard metawear) {
    var logging = metawear.GetModule<ILogging>();
    var acc = metawear.GetModule<IAccelerometer>();

    acc.Stop();
    acc.Acceleration.Stop();
    logging.Stop();

    metawear.GetModule<ISettings>().EditBleConnParams(maxConnInterval: 7.5f);
    await Task.Delay(1500);
    await logging.DownloadAsync();
}

After you have update your code with the 2 functions, run the code with your device.

>>> dotnet run -- NetCoreExamples.LogAccelerometer E8:C9:8F:52:7B:07

3.7. Anonymous Routes

In the Log Accelerometer tutorial, the data route setup and log download occured with the same IMetaWearBoard object. Sometimes the original IMetaWearBoard object (or it serialized state) is unavailable meaning you will not be able to attach data handlers as you would in the aforementioned tutorial. When this occurs, you can create anonymous routes instead.

Because you do not know how the loggers were setup, you will need identifiers to distinguish which route corresponds to what logged data. Identifiers can be retrieved from the data route in the C# SDK as seen in the documentation. If you are doing a cross platform download, you will need to get the identifiers from the SDK that setup the logger.

Lets first create the AnonymousRoutes class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using MbientLab.MetaWear.Core;
using System;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class AnonymousRoutes {
        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);

            var logging = metawear.GetModule<ILogging>();
            logging.Stop();

            Console.WriteLine("Creating anonymous routes");
            var routes = await metawear.CreateAnonymousRoutesAsync();

            Console.WriteLine($"{routes.Count} active loggers discovered");
            foreach (var r in routes) {
                r.Subscribe(_ =>
                    Console.WriteLine($"identifier: {r.Identifier}, time: {_.FormattedTimestamp}, data: [{BitConverter.ToString(_.Bytes).ToLower().Replace("-", ", 0x")}]")
                );
            }

            metawear.GetModule<ISettings>().EditBleConnParams(maxConnInterval: 7.5f);
            await Task.Delay(1500);
            await logging.DownloadAsync();

            metawear.GetModule<IMacro>().EraseAll();
            var debug = metawear.GetModule<IDebug>();
            debug.ResetAfterGc();
            await debug.DisconnectAsync();
        }
    }
}

The IAnonymousRoute objects returned on line 14 represent the board’s current logging state. You can subscribe to them and subsequently receive the data from that logger. As mentioned above, a string identifier tells the user where the logged data is coming from so you can properly extra the value from the IData object.

To see an effective example of this feature, start logging some data with the MetaBase app then run the AnonymousRoutes code.

>>> dotnet run -- NetCoreExamples.AnonymousRoutes E8:C9:8F:52:7B:07

3.8. Data Processor

A powerful feature of the firmware is its ability to process data on-board before passing it to the next consumer. We will demonstrate this feature by using the fuser to instruct the firmware combine data from the accelerometer and gyro into 1 message.

Add the DataProcessor class to the project with the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using MbientLab.MetaWear;
using MbientLab.MetaWear.Core;
using MbientLab.MetaWear.Data;
using MbientLab.MetaWear.Sensor;
using MbientLab.MetaWear.Sensor.GyroBmi160;
using System;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class DataProcessor {
        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);

            Console.WriteLine($"Configuring {args[0]}...");
            var acc = metawear.GetModule<IAccelerometer>();
            var gyro = metawear.GetModule<IGyroBmi160>();

            acc.Configure(odr: 25f);
            gyro.Configure(odr: OutputDataRate._25Hz);

            await gyro.AngularVelocity.AddRouteAsync(source => source.Buffer().Name("gyro"));
            await acc.Acceleration.AddRouteAsync(source => source.Fuse("gyro").Stream(_ => {
                var array = _.Value<IData[]>();

                // accelerometer is the source input, index 0
                // gyro name is first input, index 1
                Console.WriteLine($"acc = {array[0].Value<Acceleration>()}, gyro = {array[1].Value<AngularVelocity>()}");
            }));

            gyro.AngularVelocity.Start();
            acc.Acceleration.Start();

            gyro.Start();
            acc.Start();

            await Task.Delay(15000);

            Console.WriteLine("Resetting device");
            await metawear.GetModule<IDebug>().ResetAsync();
        }
    }
}

Specifically for the fuser, you first need to direct all of the other data to a named buffer component, as seen on line 21. For this example, the gyro is our “other” data and we have named its buffer “gyro”.

On line 22, we pass that name into the Fuse function then stream its output to the host device.

Because the data is fused, it is treated as an IData array whose indices math the order of the buffer names passed in, with the source data being index 0.

After setting up the data routes, we can start the sensors and watch the combined data stream to our host device.

>>> dotnet run -- NetCoreExamples.DataProcessor E8:C9:8F:52:7B:07

3.9. Events

Note

This tutotrial will make use of the IDataProcessor interface

Another neat feature for offline use is the event system. This system lets you program the board to react to events by executing MetaMotion commands. In fact, the timed task that we created back in the Stream Temperature tutorial internally uses the event system to execute the Read command on each timer event.

To have the board react to data, we attach a React component to the target data source.

For this tutorial, we will use the switch state to cycle through each LED color by counting the number of times it was pressed.

Start the LedController class and the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using MbientLab.MetaWear;
using MbientLab.MetaWear.Builder;
using MbientLab.MetaWear.Core;
using MbientLab.MetaWear.Core.DataProcessor;
using MbientLab.MetaWear.Peripheral;
using MbientLab.MetaWear.Peripheral.Led;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class LedController {
        internal static async Task Setup(IMetaWearBoard metawear) {
            throw new NotImplementedException();
        }

        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);

            Console.WriteLine($"Configuring {args[0]}...");
            await Setup(metawear);

            Console.WriteLine("Press [Enter] to reset the board");
            Console.ReadLine();

            Console.WriteLine("Resetting device...");
            await metawear.GetModule<IDebug>().ResetAsync();
        }
    }
}

For the control logic, we will cycle through 4 states (3 LED colors and LED off) which means there are 4 comparison filters with their own React components. The reaction to values [1-3] is to turn on the green, red, then blue LEDs respectively. For value 4, the reaction will only be to turn off the LED and reset the counter to 0.

In code, the LedController.Setup function looks like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
internal static async Task Setup(IMetaWearBoard metawear) {
    var mwSwitch = metawear.GetModule<ISwitch>();
    await mwSwitch.State.AddRouteAsync(source => {
        var led = metawear.GetModule<ILed>();
        var mod = source.Filter(Comparison.Eq, 1).Count().Name("press-count");

        var colors = Enum.GetValues(typeof(Color)).Cast<Color>();
        mod.Filter(Comparison.Eq, colors.Count() + 1).React(token => {
            led.Stop(true);
            metawear.GetModule<IDataProcessor>().Edit<ICounterEditor>("press-count").Reset();
        });
        foreach (var c in colors) {
            mod.Filter(Comparison.Eq, ((int)c) + 1).React(token => {
                led.Stop(true);
                led.EditPattern(c, Pattern.Solid);
                led.Play();
            });
        }
    });
}

Pay extra attention to lines 5 and 10. Data processor components can have a name associated with them, which you then use with the IDataProcessor interface. For this data processing chain, we assign the counter the “press-count” name, then edit its state as part of the LED stop reaction.

Run the LedController code and control the LED with the switch: >>> dotnet run – NetCoreExamples.LedController E8:C9:8F:52:7B:07

3.10. Macros

When you have finalized your commands, you may want to save them to the board so you do not need to reprogram them after every power cycle. This is where the IMacro interface comes in handy.

3.10.1. Setup

Combining the macro system with your existing code is very straightforward. Lets see how to do this by saving the LED controller code from the Events section to the board.

Add MacroSetup.cs to the project with the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using MbientLab.MetaWear.Builder;
using MbientLab.MetaWear.Core;
using MbientLab.MetaWear.Peripheral;
using MbientLab.MetaWear.Peripheral.Led;
using MbientLab.MetaWear.Sensor;
using System;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class MacroSetup {
        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);
            var macro = metawear.GetModule<IMacro>();

            Console.WriteLine($"Configuring {args[0]}...");
            macro.StartRecord();
            await LedController.Setup(metawear);
            await macro.EndRecordAsync();

            Console.WriteLine("Resetting device");
            await metawear.GetModule<IDebug>().ResetAsync();
        }
    }
}

When comparing the LedController.RunAsync with MacroSetup.RunAsync, you can see that they are almost identical except the macro version wraps the call to Setup with StartMacro and EndRecordAsync.

Run the macro setup code and see that the push-button switch will always control the LED, even if you reset the board.

>>> dotnet run -- NetCoreExamples.MacroSetup E8:C9:8F:52:7B:07

3.10.2. Removing Macros

Eventually, you may want to repurpose the board and have it do something new. Because the macro persists the commands in the flash memory, you will need to issue a macro erase command combined with a debug flag that resets the board after garbage collection.

Create a MacroErase.cs file with following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
using MbientLab.MetaWear.Core;
using System;
using System.Threading.Tasks;

namespace NetCoreExamples {
    class MacroRemove {
        static async Task RunAsync(string[] args) {
            var metawear = await ScanConnect.Connect(args[0]);

            Console.WriteLine("Removing macros");
            metawear.GetModule<IMacro>().EraseAll();

            var debug = metawear.GetModule<IDebug>();
            debug.ResetAfterGc();
            await debug.DisconnectAsync();
        }
    }
}

The erase command is issued at line 11 (EraseAll) followed by the the reset and disconnect calls at line 14 and 15..

>>> dotnet run -- NetCoreExamples.MacroErase E8:C9:8F:52:7B:07