Twitter icon
Facebook icon
LinkedIn icon
Google icon
Reddit icon
StumbleUpon icon
Del.icio.us icon

Send Data From Meadow to an ASP. NET Server via WiFi

Added to IoTplaybook or last updated on: 12/28/2020
Send Data From Meadow to an ASP. NET Server via WiFi

Story

In this project, we're going to learn how to connect Meadow to a WiFi network and log the room temperature with an LM35 analog temperature sensor sending the data periodically to an ASP.NET server running on a PC.

Everything you need to build this project is included in the Wilderness Labs Meadow F7 w/Hack Kit Pro. We'll see how easy is to program these peripherals using Meadow.Foundation.

Meadow.Foundation is a platform for quickly and easily building connected things using.NET on Meadow. Created by Wilderness Labs, it's completely open-source and maintained by the Wilderness Labs community.

IMPORTANT NOTE: For this project, we're gonna need the latest version (b4.0) of Meadow.OS running on our boards Learn how to upgrade your Meadow to the latest version using this guide.

Things used in this project

Hardware components

Meadow F7 Micro
Wilderness Labs Meadow F7 Micro
 
× 1

Wilderness Labs

Meadow F7 Micro Development kit w/Hack Kit Pro.
Wilderness Labs Meadow F7 Micro Development kit w/Hack Kit Pro.
 
× 1

Wilderness Labs

 
ST7789 TFT SPI Display
 
× 1  
 
LM35 Analog Temperature Sensor
 
× 1  

Software apps and online services

 
Microsoft Visual Studio 2019
 
  Microsoft

Step 1 - Setting up the Clima Server

Go to the Clima github repo and download it on a zip (or clone it if you wish). Open the Clima.ServerHosts.Mini solution and run the project named WildernessLabs.Clima.Server.Mini on your computer. It should open a Command Prompt window with information about the app. Take note of the port number, as you will need this later on your Meadow application later on.

Clima Server running

Clima Server running

To test the server handling requests, you can use an app to request data to this server like Postman. Make a GET request to the following URL:

http://localhost:[PORT NUMBER]/ClimateData

You should see it will return the following sample data in json format:

[
    {
        "id": 1,
        "tempC": 22,
        "barometricPressureMillibarHg": 200,
        "relativeHumdity": 0.5
    }
]

If you are getting the sample data above, your server is all set and you can continue on building the Meadow client project. Keep this project running and open a new instance of visual studio to create and run the Meadow client project.

Step 2 - Building the Meadow Clima Client

Circuit Diagram

Wire your project like the following Fritzing diagram:

Clima Circuit Diagram
Clima Circuit Diagram

Step 2.1 - Create a Meadow Application project

Create a new Meadow Application project in Visual Studio 2019 for Windows or macOS and name it Clima.

Step 2.2 - Add the required NuGet packages

For this project, search and install the following NuGet packages:

Step 2.3 - Add an image asset

Download the following image (meadow.jpg), and add it to your project:

meadow.jpg
meadow.jpg

Step 2.4 - Write the code for Clima

For this project, we're going to need to add a few classes. We'll go through each one and explain the role they play on the app.

DisplayController class

Add a new class and name it DisplayController and copy the code below:

public class DisplayController
{
    protected St7789 display;
    protected GraphicsLibrary graphics;
    protected AtmosphericConditions conditions;

    protected bool isRendering = false;
    protected object renderLock = new object();

    public DisplayController()
    {
        InitializeDisplay();
    }

    protected void InitializeDisplay()
    {
        var config = new SpiClockConfiguration(
            6000, SpiClockConfiguration.Mode.Mode3);
        display = new St7789
        (
            device: MeadowApp.Device,
            spiBus: MeadowApp.Device.CreateSpiBus(
                MeadowApp.Device.Pins.SCK, 
                MeadowApp.Device.Pins.MOSI, 
                MeadowApp.Device.Pins.MISO, 
                config),
            chipSelectPin: null,
            dcPin: MeadowApp.Device.Pins.D01,
            resetPin: MeadowApp.Device.Pins.D00,
            width: 240, height: 240
        );

        graphics = new GraphicsLibrary(display) {
            CurrentFont = new Font12x20(),
        };

        graphics.Clear(true);

        //Render();
    }

    public void UpdateDisplay(AtmosphericConditions conditions) {
        this.conditions = conditions;
        Render();  
    }

    protected void Render()
    {
        lock (renderLock) 
        {
            if (isRendering) 
            {
                Console.WriteLine("Already in a rendering loop, bailing out.");
                return;
            }

            isRendering = true;
        }

        graphics.Clear(true);

        graphics.Stroke = 1;
        graphics.DrawRectangle(
            xLeft: 0, 
            yTop: 0, 
            width: (int)display.Width, 
            height: (int)display.Height, 
            color: Color.White);
        graphics.DrawRectangle(
            xLeft: 5, 
            yTop: 5, 
            width: (int)display.Width - 10, 
            height: (int)display.Height - 10, 
            color: Color.White);

        graphics.DrawCircle(
            centerX: (int)display.Width / 2, 
            centerY: (int)display.Height / 2, 
            radius: (int)(display.Width / 2) - 10, 
            color: Color.FromHex("#23abe3"), 
            filled: true);

        DisplayJPG(55, 40);

        string text = $"{conditions.Temperature?.ToString("##.#")}°C";
        graphics.CurrentFont = new Font12x20();
        graphics.DrawText(
            x: (int)(display.Width - text.Length * 24) / 2, 
            y: 140, 
            text: text, 
            color: Color.Black, 
            scaleFactor: GraphicsLibrary.ScaleFactor.X2);

        graphics.Rotation = GraphicsLibrary.RotationType._270Degrees;

        graphics.Show();

        isRendering = false;
    }

    protected void DisplayJPG(int x, int y)
    {
        var jpgData = LoadResource("meadow.jpg");
        var decoder = new JpegDecoder();
        var jpg = decoder.DecodeJpeg(jpgData);

        int imageX = 0;
        int imageY = 0;
        byte r, g, b;

        for (int i = 0; i < jpg.Length; i += 3)
        {
            r = jpg[i];
            g = jpg[i + 1];
            b = jpg[i + 2];

            graphics.DrawPixel(imageX + x, imageY + y, Color.FromRgb(r, g, b));

            imageX++;
            if (imageX % decoder.Width == 0)
            {
                imageY++;
                imageX = 0;
            }
        }

        display.Show();
    }

    protected byte[] LoadResource(string filename)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = $"Clima.Meadow.HackKit.{filename}";

        using (Stream stream = assembly.GetManifestResourceStream(resourceName))
        {
            using (var ms = new MemoryStream())
            {
                stream.CopyTo(ms);
                return ms.ToArray();
            }
        }
    }
}

DisplayController works as an abstraction to encapsulate all the logic related to load and draw the user interface to the ST7789 SPI display. It's comprised of the following methods:

  • InitializeDisplay - this method has all the logic to initialize both the display and the graphics library which is used to draw the UI in the Render method.
  • UpdateDisplay - Public method that updates the temperature value and calls Render to redraw the UI.
  • Render - Draws the entire UI along with the Temperature value.
  • DisplayJPG - this method uses the SimpleJpegDecoder to decode the array of bytes returned from LoadResource method, which are assigned to the jpg byte array that has all the red, green, and blue values on each pixel one after another.
  • LoadResource - Opens the image file to get the stream and returns an array of bytes of raw data.

ClimateServiceFacade class

Add a static class named ClimateServicesFacade and copy the code below:

public static class ClimateServiceFacade
{        
    static string climateDataUri = "http://[IP ADDRESS HERE]:2792/ClimateData";

    static ClimateServiceFacade() { }

    /// <summary>
    /// Posts a temperature reading to the web API endpoint
    /// </summary>
    /// <param name="tempC"></param>
    /// <returns></returns>
    public static async Task PostTempReading(decimal tempC)
    {
        ClimateReading climateReading = new ClimateReading() { TempC = tempC };

        using (HttpClient client = new HttpClient()) {
            client.Timeout = new TimeSpan(0, 5, 0);

            string json = System.Text.Json.JsonSerializer
                .Serialize<ClimateReading>(climateReading);

            HttpResponseMessage response = await client.PostAsync(
                climateDataUri, new StringContent(
                    json, Encoding.UTF8, "application/json"));
            try {
                response.EnsureSuccessStatusCode();
            } catch (TaskCanceledException) {
                Console.WriteLine("Request time out.");
            } catch (Exception e) {
                Console.WriteLine($"Request went sideways: {e.Message}");
            }
        }
    }

    /// <summary>
    /// Fetches the climate readings from the Web API Endpoint
    /// </summary>
    /// <returns></returns>
    public static async Task FetchReadings()
    {
        using (HttpClient client = new HttpClient()) {
            client.Timeout = new TimeSpan(0, 5, 0);

            HttpResponseMessage response = await client.GetAsync(climateDataUri);

            try {
                response.EnsureSuccessStatusCode();

                //System.Json[old skool]
                string json = await response.Content.ReadAsStringAsync();

                Console.WriteLine(json);

                var stuff = System.Text.Json.JsonSerializer
                    .Deserialize(json, typeof(ClimateReading[]));

                Console.WriteLine("deserialized to object");

                var reading = stuff as ClimateReading[];

                Console.WriteLine($"Temp: {reading[0].TempC}");
            } catch (TaskCanceledException) {
                Console.WriteLine("Request time out.");
            } catch (Exception e) {
                Console.WriteLine($"Request went sideways: {e.Message}");
            }
        }
    }
}

This static method is used to make send the GET/POST requests to the server:

  • PostTempReading - Posts a temperature reading to the web API endpoint. Notice how we're using an HttpClient just like we would on a mobile or desktop app, and we're using System.Text.Json to serialize our temperature value to send the request to the server.
  • FetchReadings - this method gets the climate readings from the Web API Endpoint. We're again using an HttpClient to a GET request, and System.Text.Json to deserialize the server's response, and printing the value in the output console.

ClimateReading class

Add a ClimateReading class and copy the following code:

public class ClimateReading
{
    public long? ID { get; set; }
    public decimal? TempC { get; set; }
    public decimal? BarometricPressureMillibarHg { get; set; }
    public decimal? RelativeHumdity { get; set; }
}

This is a model class we'll use to send data to the ASP.NET server project.

Secrets class

Add a Secrets class and copy the code below:

public class Secrets
{
    /// <summary>
    /// Name of the WiFi network to use.
    /// </summary>
    public const string WIFI_NAME = string.Empty;

    /// <summary>
    /// Password for the WiFi network names in WIFI_NAME.
    /// </summary>
    public const string WIFI_PASSWORD = string.Empty;
}

Initialize both WIFI_NAME and WIFI_PASSWORD fields with your WiFi credentials.

MeadowApp class

In your main MeadowApp class, copy the following code:

public class MeadowApp : App<F7Micro, MeadowApp>
{
    AnalogTemperature analogTemperature;
    DisplayController displayController;

    public MeadowApp()
    {
        Initialize();                        

        var tA = ReadTemp();
        tA.Wait();
        var conditions = tA.Result;

        this.displayController.UpdateDisplay(conditions);

        ClimateServiceFacade.FetchReadings().Wait();

        ClimateServiceFacade.PostTempReading((decimal)conditions.Temperature).Wait();

        ClimateServiceFacade.FetchReadings().Wait();

        Console.WriteLine("App finished running.");
    }

    void Initialize()
    {
        var rgbLed = new RgbLed(
            Device, 
            Device.Pins.OnboardLedRed, 
            Device.Pins.OnboardLedGreen, 
            Device.Pins.OnboardLedBlue
        );
        rgbLed.SetColor(RgbLed.Colors.Red);

        analogTemperature = new AnalogTemperature(
            device: Device,
            analogPin: Device.Pins.A00,
            sensorType: AnalogTemperature.KnownSensorType.LM35
        );

        displayController = new DisplayController();

        rgbLed.SetColor(RgbLed.Colors.Blue);

        Device.InitWiFiAdapter().Wait();

        var result = Device.WiFiAdapter.Connect(
            Secrets.WIFI_NAME, Secrets.WIFI_PASSWORD);
        if (result.ConnectionStatus != ConnectionStatus.Success)
        {
            rgbLed.SetColor(RgbLed.Colors.Magenta);
            throw new Exception($"Cannot connect to network: 
                {result.ConnectionStatus}");
        }

        rgbLed.SetColor(RgbLed.Colors.Green);
    }

    protected async Task<AtmosphericConditions> ReadTemp()
    {
        var conditions = await analogTemperature.Read();
        return conditions;
    }
}

In the MeadowApp Main class, you can notice the logic is split into the following methods:

  • Constructor - First thing the Meadow Application does is call the Initialize method to set up all the peripherals and network connectivity. After that, it calls the ReadTemp method to get the current room temperature from the analog sensor. Next you'll see the app use the ClimateServiceFacade static class to get a list of reading values from the server with the FetchReadings method proceeds to call PostTempReading to send the current temperature value to the server and finally calls FetchReadings to get the most recent list of readings from the server, and the app finally finishes executing.
  • Initialize - This method first initializes the onboard RGB LED and sets the color to red to indicate the app has started the initialize process. Then the app proceeds to initialize the analog temperature sensor and instantiated a DisplayController object, setting the onboard RGB LED to blue to indicate that its now ready to connect to the WiFi network.If there was a problem trying to connect to the network, the LED will turn magenta and you should see the Exception thrown in your output console. Otherwise, the LED will turn green, and the project is now fully initialized.
  • ReadTemp - it will call an asynchronous Read method on the AnalogTemperature sensor and return an AtmosphericConditions value.

Step 3 - Run the project

While your server project is running, click the Run button in Visual Studio to deploy the Meadow app to your board, and if your Meadow is successfully connected to the network and is able to use the LM35 analog sensor, you should see your room temperature on the display:

Clima project running...
Clima project running...

Check out Meadow.Foundation!

This project is only the tip of the iceberg in terms of the extensive exciting things you can do with Meadow.Foundation.

  • It comes with a huge peripheral driver library with drivers for the most common sensors and peripherals.
  • The peripheral drivers encapsulate the core logic and expose a simple, clean, modern API.
  • This project is backed by a growing community that is constantly working on building cool connected things and are always excited to help new-comers and discuss new projects.

References

Schematics

Clima circuit diagram

 

Meadow Logo

 

Code

Clima complete solution

WildernessLabs / Clima

Climate Station App — Read More

Latest commit to the master branch on 12-17-2020 - Download as zip

Credits

Jorge Ramírez

Jorge Ramirez

Software developer getting into Hardware in the .Net world! Music lover, guitar enthusiast, intermediate hiker, and bike commuter!

 

Adrian Stevens

Adrian Stevens

 

bryan costanich

Bryan Costanich

 

Hackster.io

This content is provided by our content partner Hackster.io, an Avnet developer community for learning, programming, and building hardware. Visit them online for more great content like this.

This article was originally published at Hackster.io. It was added to IoTplaybook or last modified on 12/28/2020.