Meadow Getting Started

I have acquired a ProjectLab device with Meadow from Wilderness Labs. This post describes how to set up a development environment on Windows for such a device.

Installation

Install the latest .NET SDK. In my case at the time of writing, I am testing with .NET 8 Preview 3, hence that is installed my machine.

Next install the Meadow dotnet CLI. It is a dotnet tool, so we can install it as:

dotnet tool install WildernessLabs.Meadow.CLI --global

To flash the OS on the Meadow, we will need to connect to it in DFU Bootloader and use the dfu-util to copy the new binaries. A full documentation at the time is available here.

From Admin Console Install dfu-utils using the meadow CLI:

meadow install dfu-util

Once done, we can use Zadig to make sure that the driver for the STM32 Bootloader device is WinUSB (v.6.1.7600.16385). Use "Replace driver" button in Zadig to update, if that is not the case.FIn Windows the device may appear either under the COM ports or under USB devices as STM32 BOOTLOADER. Bootloader mode is only required for flashing the OS, in non-bootloader mode it will show under Ports (USB Serial Device COMx).

If Windows does not see the device under Ports as COMx in non-bootload mode, it probably sees it as a USB device. Uninstall Device from 'Universal Serial Bus devices' while checking the "Attempt to remove the driver for this device" too. Unplug and re-plug device and it shall show up on COMx under Ports.

Fetch the latest OS for the meadow device and flash it:

meadow download os
meadow flash os

In case getting a timeout during flashing the device, run the following command in non-dfu mode: meadow flash erase. Then meadow flash os. Repeat the process if necessary.

Setup Visual Studio Code

Install the Meadow extension for VS Code. This enables us to build the project in Debug configuration, as well as to run and debug the application code.

Using VS Code, add launch.json so that the application can be started and debugged:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Deploy",
            "type": "meadow",
            "request": "launch",
            "preLaunchTask": "meadow: Build"
        }
    ]
}

Add tasks.json to Build and Deploy in release mode as well as to Stop the running application.

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Deploy App",
            "type": "shell",
            "command": "meadow app deploy -f .\\bin\\Release\\netstandard2.1\\App.dll",
            "group": "none",
            "problemMatcher": [],
            "dependsOn": [
                "Build Release"
            ]
        },
        {
            "label": "Build Release",
            "type": "shell",
            "command": "dotnet build --configuration Release",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": []
        },
        {
            "label": "Stop Application",
            "type": "shell",
            "command": "meadow mono disable",
            "group": "none",
            "problemMatcher": []
        }
    ]
}

In VS code open the source code of the application and use F5 to build and run the application. It took multiple rounds for the first run as the CLI timed out on copying larger files.

Stop Application from CLI

meadow mono disable

Wi-Fi Setup

Set device name in meadow.config.yaml. Also enable here for the ESP32 coprocessor to connect automatically to the network. I also enabled NTP and DNS servers with the default configuration.

Add a wifi.config.yaml:

Credentials:
    Ssid: MY-NETWORK
    Password: network-pass

Finally MeadowApp.cs make sure to have the Wi-Fi enabled before the projectlab.Create() is invoked:

 var wifi = Device.NetworkAdapters.Primary<IWiFiNetworkAdapter>();
wifi.NetworkConnected += NetworkConnected;
//...
void NetworkConnected(INetworkAdapter sender, NetworkConnectionEventArgs args) => Resolver.Log.Trace($"IP: {sender.IpAddress}");

Connecting with IoT Hub in Azure

Create a new Device in Azure. To connect from the Meadow, we can use AMQP protocol. We need a SAS token for this protocol. One can generate the SAS token by following this documentation, or by the sample code below.

Copy the Device Id, Device Primary Key and Hub name from IoT Hub to make the below code work:

private SenderLink _sender;

public async Task StartAsync(string hubName, string deviceId, string devicePrimaryKey)
{
    try
    {
        Connection.DisableServerCertValidation = false;
        
        // parse Azure IoT Hub Map settings to AMQP protocol settings
        var hostName = hubName + ".azure-devices.net";
        var userName = deviceId + "@sas." + hubName;
        var token = GenerateSasToken($"{hostName}/devices/{deviceId}", devicePrimaryKey, string.Empty);

        var factory = new ConnectionFactory();
        var connection = await factory.CreateAsync(new Address(hostName, 5671, userName, token));
        var session = new Session(connection);
        _sender = new SenderLink(session, "send-link", $"devices/{deviceId}/messages/events");
        var receiver = new ReceiverLink(session, "receive-link", $"devices/{deviceId}/messages/deviceBound");
        receiver.Start(100, OnMessage);
    }
    catch (Exception ex)
    {
        Resolver.Log.Error(ex.Message);
    }
}

private void OnMessage(IReceiverLink receiver, Message message)
{
    if (message.Body is byte[] data)
    {
        // Handle message...
        receiver.Accept(message);
    }
}

public void Send(string data)
{
    var message = new Message(data);
    message.ApplicationProperties = new Amqp.Framing.ApplicationProperties();
    _sender.Send(message);
}

private string GenerateSasToken(string resourceUri, string key, string policyName, int expiryInSeconds = 3600)
{
    var fromEpochStart = DateTime.UtcNow - new DateTime(1970, 1, 1);
    var expiry = Convert.ToString((int)fromEpochStart.TotalSeconds + expiryInSeconds);

    var stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + expiry;

    var hmac = new HMACSHA256(Convert.FromBase64String(key));
    var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));

    var token = string.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", WebUtility.UrlEncode(resourceUri), WebUtility.UrlEncode(signature), expiry);

    if (!string.IsNullOrEmpty(policyName))
    {
        token += "&skn=" + policyName;
    }

    return token;
}

Using Native Binaries

Given a library compiled to ARM Cortex M7 and C/C++ header files. Use ClangSharpPInvokeGenerator to generate the DllImport (or LibraryImport) method definitions. DllImport indicates that the method is exposed by an unmanaged DLL as a static entry point.

For example, use the following command to generate the C# bindings for the BSEC library used by the BME688 sensor:

ClangSharpPInvokeGenerator --config generate-file-scoped-namespaces generate-helper-types --file bsec_datatypes.h bsec_interface.h bsec_interface_multi.h --methodClassName BSecProgram --namespace BSec.Interop --output ./BSEC/Interop.cs --libraryPath BSECLibrary64.dll

However, note that the current Release Candidate 3 does not yet support interop, as it does not have a fully implemented dynamic loader to load native files. When running the application and trying to invoke one of the methods that is exposed by the native library, we get an error:

Mono: DllImport unable to load library 'libalgobsec.lib'.
Meadow StdOut: App Error in App Run: libalgobsec.lib assembly:<unknown assembly> type:<unknown type> member:(null)
Meadow StdOut: System.DllNotFoundException: libalgobsec.lib assembly:<unknown assembly> type:<unknown type> member:(null)
Meadow StdOut: at (wrapper managed-to-native) BSec.Interop.BSecProgram.bsec_init()

Looking forward to future releases that will likely fix the issue.

Note that the BME688 sensor uses heating profiles (to heat a metal plate) which can sense Gas resistance on different temperatures. By default, the sensor comes with a native library that uses these heating profiles to determine Air Quality Index, VOC, CO2 levels. However, this code is supported by a native closed source library using Machine Learning. The C++ samples provided by the manufacturer of the sensor can be found on GitHub.