This project shows you how to run Meadow applications straight from a Windows machine using Meadow.Windows. You can even connect physical peripherals and/or sensors using an IO Expander like Adafruit's FT232H. As an example in this project, we'll wire up an LCD 20x4 character display and control it using Meadow.Foundation.
Meadow.Foundation 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.
Step 1 - Register and create a OpenWeather API KeyBefore we start working on our Meadow project, we first need to create an account in the OpenWeatherMap site to get a API Key so we can call the API to get the weather data. Once signed up, go to the API section, and click Subscribe on the Current Weather Data API. You can also check the API doc to have a better understanding of the URL your Meadow Client can call, along with all the different parameters you can send over t
This takes you to the pricing section. Lets click Get API Key on the Free tier, since we just plan to use this for test purposes.
Finally, go ahead and copy your API Key somewhere locally. You will need to send it when doing a GET request
Step 2 - Assemble the circuitWire your project like this:
Create a new.NET Console application project, and open the.csproj file and replace it with the following
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Disclaimer: we currently don't have a Meadow.Windows template, so this the only way at this time to create Meadow applications that run on Windows.
Step 4 - Add the required NuGet packagesFor this project, search and install the following NuGet packages:
Step 5 - Add FT232H native library to your projectAdd the native library (libmpsse.dll) of the FT232H IO Expander depending on your CPU's architecture (Win32 or x64) to your project and set th Copy to Output Directory to Copy if newer or Copy always.
Next, download these seven weather icons from our repo:
Add them to your project. Make sure you go through each one's properties and set its Build Action to Embedded Resource.
Step 7 - Write the code for the Meadow.Windows projectThe structure for this project looks like this:
We will go through each folder, and at the same time we can add the class and copy over the logic for each file to put the project together.
Models Folder
In the Models folder, we have a Constants class, which has all the weather codes that OpenWeather returns from their GetCurrentWeather API.
WeatherReading class is a just a Data Transfer Object (DTO) we use to deserialize the API response.
Services Folder
Inside Services folder we have WeatherService, which is a static class that its sole purpose is to instantiate an HttpClient and send a GET request to OpenWeather API to give us the current weather.
Copy the following code below:
public static class WeatherService
{
static string climateDataUri = "http://api.openweathermap.org/data/2.5/weather";
static WeatherService() { }
public static async Task<WeatherReading> GetWeatherForecast()
{
using (HttpClient client = new HttpClient())
{
try
{
client.Timeout = new TimeSpan(0, 5, 0);
HttpResponseMessage response = await client.GetAsync($"{climateDataUri}?q={Secrets.WEATHER_CITY}&appid={Secrets.WEATHER_API_KEY}");
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
var values = JsonSerializer.Deserialize<WeatherReading>(json);
return values;
}
catch (TaskCanceledException)
{
Console.WriteLine("Request timed out.");
return null;
}
catch (Exception e)
{
Console.WriteLine($"Request went sideways: {e.Message}");
return null;
}
}
}
}
Notice on the GetAsync we're passing the API Key we generated earlier along with a city to get local weather area.
VIewModels Folder
In this folder we have a WeatherViewModel class, which receives the deserialized json object and we extract and format the data with the relevant fields we want to show on our display.
public class WeatherViewModel
{
public int WeatherCode { get; set; }
public int OutdoorTemperature { get; set; }
public int FeelsLikeTemperature { get; set; }
public int Pressure { get; set; }
public int Humidity { get; set; }
public decimal WindSpeed { get; set; }
public int WindDirection { get; set; }
public WeatherViewModel(WeatherReading outdoorConditions)
{
WeatherCode = outdoorConditions.weather[0].id;
OutdoorTemperature = (int)(outdoorConditions.main.temp - 273);
FeelsLikeTemperature = (int)(outdoorConditions.main.feels_like - 273);
Pressure = outdoorConditions.main.pressure;
Humidity = outdoorConditions.main.humidity;
WindSpeed = outdoorConditions.wind.speed;
WindDirection = outdoorConditions.wind.deg;
}
}
The temperature value that's in OutdoorConditions object comes in Kelvin degrees, so we substract 273 to convert it to Celsius
.
Views Folder
In the Views folder, we have the DisplayView class, which is in charge in receiving the formatted data and displaying it on the ILI9488 display.
public class DisplayView
{
MicroGraphics graphics;
int x_padding = 5;
Color backgroundColor = Color.FromHex("#F3F7FA");
Color foregroundColor = Color.Black;
Font12x20 font12X20 = new Font12x20();
Font8x16 font8X16 = new Font8x16();
public DisplayView() { }
public void Initialize(IGraphicsDisplay display)
{
graphics = new MicroGraphics(display)
{
Stroke = 1,
CurrentFont = font12X20
};
x_padding = 20;
graphics.Clear(backgroundColor);
}
public void UpdateDateTime()
{
var today = DateTime.Now;
graphics.DrawRectangle(graphics.Width / 2, 24, graphics.Width, 82, backgroundColor, true);
graphics.CurrentFont = font12X20;
graphics.DrawText(graphics.Width - x_padding, 25, $"{today.DayOfWeek},{today.Day}{GetOrdinalSuffix(today.Day)}", foregroundColor, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(graphics.Width - x_padding, 50, today.ToString("MMM"), foregroundColor, scaleFactor: ScaleFactor.X2, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(graphics.Width - x_padding, 95, today.ToString("yyyy"), foregroundColor, alignmentH: HorizontalAlignment.Right);
graphics.DrawRectangle(0, 135, graphics.Width, 35, backgroundColor, true);
graphics.DrawText(graphics.Width / 2, 135, today.ToString("hh:mm:ss tt"), foregroundColor, ScaleFactor.X2, alignmentH: HorizontalAlignment.Center);
graphics.Show();
}
private static string GetOrdinalSuffix(int num)
{
string number = num.ToString();
if (number.EndsWith("11")) return "th";
if (number.EndsWith("12")) return "th";
if (number.EndsWith("13")) return "th";
if (number.EndsWith("1")) return "st";
if (number.EndsWith("2")) return "nd";
if (number.EndsWith("3")) return "rd";
return "th";
}
public void UpdateDisplay(WeatherViewModel model)
{
int spacing = 95;
int valueSpacing = 30;
int y = 200;
graphics.Clear(backgroundColor);
DisplayJPG(model.WeatherCode, x_padding, 15);
graphics.CurrentFont = font12X20;
graphics.DrawText(x_padding, y, "Temperature", foregroundColor);
graphics.DrawText(x_padding, y + spacing, "Humidity", foregroundColor);
graphics.DrawText(x_padding, y + spacing * 2, "Pressure", foregroundColor);
graphics.DrawText(graphics.Width - x_padding, y, "Feels like", foregroundColor, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(graphics.Width - x_padding, y + spacing, "Wind Dir", foregroundColor, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(graphics.Width - x_padding, y + spacing * 2, "Wind Spd", foregroundColor, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(x_padding, y + valueSpacing, $"{model.OutdoorTemperature}°C", foregroundColor, ScaleFactor.X2);
graphics.DrawText(graphics.Width - x_padding, y + valueSpacing, $"{model.FeelsLikeTemperature + 2}°C", foregroundColor, ScaleFactor.X2, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(x_padding, y + valueSpacing + spacing, $"{model.Humidity}%", foregroundColor, ScaleFactor.X2);
graphics.DrawText(graphics.Width - x_padding, y + valueSpacing + spacing, $"{model.WindDirection}°", foregroundColor, ScaleFactor.X2, alignmentH: HorizontalAlignment.Right);
graphics.CurrentFont = font8X16;
graphics.DrawText(graphics.Width - x_padding, y + valueSpacing + spacing * 2, $"{model.WindSpeed}m/s", foregroundColor, ScaleFactor.X2, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(x_padding, y + valueSpacing + spacing * 2, $"{model.Pressure}hPa", foregroundColor, ScaleFactor.X2);
graphics.Show();
}
void DisplayJPG(int weatherCode, int xOffset, int yOffset)
{
var jpgData = LoadResource(weatherCode);
var decoder = new JpegDecoder();
var jpg = decoder.DecodeJpeg(jpgData);
int x = 0;
int y = 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(x + xOffset, y + yOffset, Color.FromRgb(r, g, b));
x++;
if (x % decoder.Width == 0)
{
y++;
x = 0;
}
}
}
byte[] LoadResource(int weatherCode)
{
var assembly = Assembly.GetExecutingAssembly();
string resourceName;
switch (weatherCode)
{
case int n when (n >= WeatherConstants.THUNDERSTORM_LIGHT_RAIN && n <= WeatherConstants.THUNDERSTORM_HEAVY_DRIZZLE):
resourceName = $"WifiWeather.w_storm.jpg";
break;
case int n when (n >= WeatherConstants.DRIZZLE_LIGHT && n <= WeatherConstants.DRIZZLE_SHOWER):
resourceName = $"WifiWeather.w_drizzle.jpg";
break;
case int n when (n >= WeatherConstants.RAIN_LIGHT && n <= WeatherConstants.RAIN_SHOWER_RAGGED):
resourceName = $"WifiWeather.w_rain.jpg";
break;
case int n when (n >= WeatherConstants.SNOW_LIGHT && n <= WeatherConstants.SNOW_SHOWER_HEAVY):
resourceName = $"WifiWeather.w_snow.jpg";
break;
case WeatherConstants.CLOUDS_CLEAR:
resourceName = $"WifiWeather.w_clear.jpg";
break;
case int n when (n >= WeatherConstants.CLOUDS_FEW && n <= WeatherConstants.CLOUDS_OVERCAST):
resourceName = $"WifiWeather.w_cloudy.jpg";
break;
default:
resourceName = $"WifiWeather.w_misc.jpg";
break;
}
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
return ms.ToArray();
}
}
}
}
You'll notice the view has the following methods:
Initialize
- Initializes both the display and the MicroGraphics, which is used to draw the entire UI with the UpdateDisplay method.UpdateDisplay
- Receiving the WeatherViewModel object, this method will draw the both the icon and all the text fields in its corresponding coordinates and font sizes onto the display.DisplayJPG
- It calls LoadResource to get the array of bytes of the icon to draw, and using the SimpleJpeg decoder, it will draw image pixel by pixel on the screen, with x and y offset coordinates.LoadResource
- this method will load the corresponding weather icon depending on the weatherCode provided, and will return an array of bytes to DisplayJPG.
Root Level Files
Secrets class
This class stores the Weather API Key and weather location to where to acquire the weather:
public class Secrets
{
/// <summary>
/// Open Weather Map API key.
///
/// https://blog.wildernesslabs.co/add-openweather-to-your-meadow-projects/
/// </summary>
public const string WEATHER_API_KEY = "WEATHER_API_KEY";
/// <summary>
/// City to get weather for.
/// </summary>
public const string WEATHER_CITY = "WEATHER_CITY";
}
MeadowApp class
The MeadowApp class is where the main logic happens using all the components mentioned above.
public class MeadowApp : App<Windows>
{
private DisplayView _displayController;
public override Task Initialize()
{
Console.WriteLine("Creating Outputs");
var _expander = new Ft232h();
var display = new Ili9488
(
spiBus: _expander.CreateSpiBus(),
chipSelectPin: _expander.Pins.C0,
dcPin: _expander.Pins.C2,
resetPin: _expander.Pins.C1
);
_displayController = new DisplayView();
_displayController.Initialize(display);
return Task.CompletedTask;
}
async Task GetTemperature()
{
// Get outdoor conditions
var outdoorConditions = await WeatherService.GetWeatherForecast();
// Format indoor/outdoor conditions data
var model = new WeatherViewModel(outdoorConditions);
// Send formatted data to display to render
_displayController.UpdateDisplay(model);
}
public override async Task Run()
{
await GetTemperature();
while (true)
{
if (DateTime.Now.Minute == 0 && DateTime.Now.Second == 0)
{
await GetTemperature();
}
_displayController.UpdateDateTime();
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
public static async Task Main(string[] args)
{
await MeadowOS.Start(args);
}
}
Initialize
- The app first instantiates the FT232H IO Expander to use its pins and SPI bus to create an Ili9488 display object, which is then passed to initialize the DisplayView.Run
- After the screen has been initialized, we can safely call the GetTemperature function that will gather the local weather data from the OpenWeather web service.After that, the app ends on an infinite while loop that will update the date and time every second, and it'll refresh weather data every minute.Main
- For Meadow.Windows applications, its required to add this Main task method that starts the MeadowOS runtime on Windows.
Click the Run button in Visual Studio. It should look like to the following GIF:
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.
Comments