This project shows you how to run Meadow.Foundation's MicroGraphics
directly on your Windows machine using WinForms
as a IGraphicsDisplay
. Once you understand how this project works, you can use this to speed up your dev cycles to make engaging HMI screens, since wont need to wait for transferring and starting the app on a physical device.
Meadow.Foundationa 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 - Create a Meadow.Windows Application projectCreate a new .NET7 Console Application project in Visual Studio 2022 for Windows and name it WinFormsMeadow
Open the project file(WinFormsMeadow.csproj) and lets make it Meadow.Windows app.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<OutputType>Exe</OutputType>
<AssemblyName>App</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Important Note: This is a temporary step because we don't have a Meadow.Windows project template yet.
Step 2 - Add the required NuGet packagesFor this project, search and install the following NuGet packages:
Step 3 - Write the code for WinFormsMeadowMeadowApp class
Copy the following code below:
WinFormsDisplay _display;
//RotatingCube views;
WiFiWeather views;
public override Task Initialize()
{
//_display = new WinFormsDisplay();
//views = new RotatingCube(_display);
// Screen size of a ILI9488 display
_display = new WinFormsDisplay(320, 480);
views = new WiFiWeather(_display);
return Task.CompletedTask;
}
public override Task Run()
{
_ = Task.Run(() =>
{
Thread.Sleep(2000);
views.Run();
});
Application.Run(_display);
return Task.CompletedTask;
}
Things to consider in this class:
- In the
MeadowApp
class signature, notice we're stablishing that this is a Meadow.Windows app when we set the app class inheritance to:App<Windows>
- In the
initialize
method, we can create aWinFormsDisplay
just like a regular physical display and we pass it toMicroGraphics
so we can draw text, shapes, etc. - It'll create a 800x600 display by default (in the case for the RotatingCube example), or you can specify
width
andheight
to match a hardware screen you plan to show your HMI on a Meadow device (for the case of the WifiWeather view). - In the
Run
method, we fire and forget theShow3dCube
method that has an infinite loop that draws a 3D cube rotating in the center of the display.
RotatingCube.cs class
Copy the code as follow this new class:
public class RotatingCube
{
Color backgroundColor = Color.FromHex("91E46C");
Color foregroundColor = Color.FromHex("000000");
private MicroGraphics _graphics = default!;
//needs cleanup - quick port from c code
private double rot, rotationX, rotationY, rotationZ;
private double rotationXX, rotationYY, rotationZZ;
private double rotationXXX, rotationYYY, rotationZZZ;
private int[,] cubeWireframe = new int[12, 3];
private int[,] cubeVertices;
public RotatingCube(WinFormsDisplay display)
{
int cubeSize = 100;
cubeVertices = new int[8, 3] {
{ -cubeSize, -cubeSize, cubeSize},
{ cubeSize, -cubeSize, cubeSize},
{ cubeSize, cubeSize, cubeSize},
{ -cubeSize, cubeSize, cubeSize},
{ -cubeSize, -cubeSize, -cubeSize},
{ cubeSize, -cubeSize, -cubeSize},
{ cubeSize, cubeSize, -cubeSize},
{ -cubeSize, cubeSize, -cubeSize},
};
_graphics = new MicroGraphics(display)
{
CurrentFont = new Font12x20(),
Stroke = 5
};
}
public void Run()
{
int originX = (int)_graphics.Width / 2;
int originY = (int)_graphics.Height / 2;
int angle = 0;
ulong frames = 0;
var start = 0;
string frameRate = "";
start = Environment.TickCount;
while (true)
{
_graphics.Clear(backgroundColor);
_graphics.DrawText(5, 5, frameRate, foregroundColor);
angle++;
for (int i = 0; i < 8; i++)
{
rot = angle * 0.0174532; //0.0174532 = one degree
//rotateY
rotationZ = cubeVertices[i, 2] * Math.Cos(rot) - cubeVertices[i, 0] * Math.Sin(rot);
rotationX = cubeVertices[i, 2] * Math.Sin(rot) + cubeVertices[i, 0] * Math.Cos(rot);
rotationY = cubeVertices[i, 1];
//rotateX
rotationYY = rotationY * Math.Cos(rot) - rotationZ * Math.Sin(rot);
rotationZZ = rotationY * Math.Sin(rot) + rotationZ * Math.Cos(rot);
rotationXX = rotationX;
//rotateZ
rotationXXX = rotationXX * Math.Cos(rot) - rotationYY * Math.Sin(rot);
rotationYYY = rotationXX * Math.Sin(rot) + rotationYY * Math.Cos(rot);
rotationZZZ = rotationZZ;
//orthographic projection
rotationXXX = rotationXXX + originX;
rotationYYY = rotationYYY + originY;
//store new vertices values for wireframe drawing
cubeWireframe[i, 0] = (int)rotationXXX;
cubeWireframe[i, 1] = (int)rotationYYY;
cubeWireframe[i, 2] = (int)rotationZZZ;
DrawVertices();
}
DrawWireframe();
_graphics.Show();
if (++frames % 1000 == 0)
{
var now = Environment.TickCount;
var et = (now - start) / 1000d;
frameRate = $"{(1000 / et):0.0}fps";
start = Environment.TickCount;
}
}
}
void DrawVertices()
{
_graphics.DrawPixel((int)rotationXXX, (int)rotationYYY);
}
void DrawWireframe()
{
_graphics.DrawLine(cubeWireframe[0, 0], cubeWireframe[0, 1], cubeWireframe[1, 0], cubeWireframe[1, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[1, 0], cubeWireframe[1, 1], cubeWireframe[2, 0], cubeWireframe[2, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[2, 0], cubeWireframe[2, 1], cubeWireframe[3, 0], cubeWireframe[3, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[3, 0], cubeWireframe[3, 1], cubeWireframe[0, 0], cubeWireframe[0, 1], foregroundColor);
//cross face above
_graphics.DrawLine(cubeWireframe[1, 0], cubeWireframe[1, 1], cubeWireframe[3, 0], cubeWireframe[3, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[0, 0], cubeWireframe[0, 1], cubeWireframe[2, 0], cubeWireframe[2, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[4, 0], cubeWireframe[4, 1], cubeWireframe[5, 0], cubeWireframe[5, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[5, 0], cubeWireframe[5, 1], cubeWireframe[6, 0], cubeWireframe[6, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[6, 0], cubeWireframe[6, 1], cubeWireframe[7, 0], cubeWireframe[7, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[7, 0], cubeWireframe[7, 1], cubeWireframe[4, 0], cubeWireframe[4, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[0, 0], cubeWireframe[0, 1], cubeWireframe[4, 0], cubeWireframe[4, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[1, 0], cubeWireframe[1, 1], cubeWireframe[5, 0], cubeWireframe[5, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[2, 0], cubeWireframe[2, 1], cubeWireframe[6, 0], cubeWireframe[6, 1], foregroundColor);
_graphics.DrawLine(cubeWireframe[3, 0], cubeWireframe[3, 1], cubeWireframe[7, 0], cubeWireframe[7, 1], foregroundColor);
}
}
- This class takes the WinForms display in the constructor and instantiates a new
MicroGraphics
object. - In the
Run
method, the app goes in an infinite while loop that it'll draw a rotating cube in the middle of the screen.
WiFiWeather.cs class
This class is more an example for an HMI screen to show the local weather forecast on a 320x480 resolution screen (like an IlI9488):
public class WiFiWeather
{
MicroGraphics graphics;
int x_padding = 5;
Meadow.Foundation.Color backgroundColor = Meadow.Foundation.Color.FromHex("#F3F7FA");
Meadow.Foundation.Color foregroundColor = Meadow.Foundation.Color.Black;
Font12x20 font12X20 = new Font12x20();
Font8x16 font8X16 = new Font8x16();
public WiFiWeather(IGraphicsDisplay display)
{
graphics = new MicroGraphics(display)
{
Stroke = 1,
CurrentFont = font12X20
};
x_padding = 20;
graphics.Clear(backgroundColor);
}
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";
}
private void DisplayJPG(string weatherIcon, int xOffset, int yOffset)
{
var jpgData = LoadResource(weatherIcon);
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, Meadow.Foundation.Color.FromRgb(r, g, b));
x++;
if (x % decoder.Width == 0)
{
y++;
x = 0;
}
}
}
private byte[] LoadResource(string weatherIcon)
{
var assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(weatherIcon))
{
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
return ms.ToArray();
}
}
}
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();
}
public void UpdateDisplay(string weatherIcon, string temperature, string humidity, string pressure, string feelsLike, string windDirection, string windSpeed)
{
int spacing = 95;
int valueSpacing = 30;
int y = 200;
graphics.Clear(backgroundColor);
DisplayJPG(weatherIcon, 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, $"{temperature}", foregroundColor, ScaleFactor.X2);
graphics.DrawText(graphics.Width - x_padding, y + valueSpacing, $"{feelsLike}", foregroundColor, ScaleFactor.X2, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(x_padding, y + valueSpacing + spacing, $"{humidity}", foregroundColor, ScaleFactor.X2);
graphics.DrawText(graphics.Width - x_padding, y + valueSpacing + spacing, $"{windDirection}", foregroundColor, ScaleFactor.X2, alignmentH: HorizontalAlignment.Right);
graphics.CurrentFont = font8X16;
graphics.DrawText(graphics.Width - x_padding, y + valueSpacing + spacing * 2, $"{windSpeed}", foregroundColor, ScaleFactor.X2, alignmentH: HorizontalAlignment.Right);
graphics.DrawText(x_padding, y + valueSpacing + spacing * 2, $"{pressure}", foregroundColor, ScaleFactor.X2);
graphics.Show();
}
public async Task Run()
{
UpdateDisplay(
weatherIcon: $"WinFormsMeadow.w_clear.jpg",
temperature: $"23°C",
humidity: $"93%",
pressure: $"1102hPa",
feelsLike: $"26°C",
windDirection: $"178°",
windSpeed: $"19m/s");
while (true)
{
UpdateDateTime();
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
}
Program.cs class
Since this is a Meadow.Windows application, we need to explicitly start MeadowOS that will run alongside the WinForms app:
public class Program
{
public static async Task Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ApplicationConfiguration.Initialize();
await MeadowOS.Start(args);
}
}
Step 4 - Run the projectClick the Run button in Visual Studio, and if you're running the cube sample, you'll have something like this:
If you're running the WiFiWeather view example, you should see something like this:
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