Project updated to beta 6.2 (released on March 5th, 2021)
The purpose of this project is to get familiar with Meadow.Foundation's MicroGraphics, a very intuitive and powerful API that can draw shapes, lines, text and images with just a few lines of code.
We will connect an SPI TFT LCD display and go through three parts to get familiar with the display graphics library: 1. Drawing basic shapes and lines, 2. Drawing texts with different font sizes, and 3. An example with a combination of both to create an analog watch face.
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.
If you're new working with Meadow, I suggest you go to the Getting Started w/ Meadow by Controlling the Onboard RGB LED project to properly set up your development environment.
Step 1 - Assemble the circuitFor this project, we're going to use a ST7789
TFT SPI
display. Connect the display to Meadow as shown in the Fritzing diagram:
Create a new Meadow Application project in Visual Studio 2019 for Windows or macOS; since our game is a variation of the game Simon, let's call it MeadowClockGraphics.
Step 3 - Add the required NuGet packagesFor this project, search and install the following NuGet packages:
Step 3.1 - Displaying basic shapesIn this first section, we'll learn how to display basic shapes and lines. In your MeadowApp class, add paste the following code:
// public class MeadowApp : App<F7Micro, MeadowApp> <- If you have a Meadow F7 v1.*
public class MeadowApp : App<F7MicroV2, MeadowApp>
{
readonly Color WatchBackgroundColor = Color.White;
MicroGraphics graphics;
int displayWidth, displayHeight;
int hour, minute, tick;
public MeadowApp()
{
Initialize();
DrawShapes();
}
void Initialize()
{
var onboardLed = new RgbPwmLed(
device: Device,
redPwmPin: Device.Pins.OnboardLedRed,
greenPwmPin: Device.Pins.OnboardLedGreen,
bluePwmPin: Device.Pins.OnboardLedBlue);
onboardLed.SetColor(Color.Red);
var config = new SpiClockConfiguration(
speed: new Frequency(48000, Frequency.UnitType.Kilohertz),
mode: SpiClockConfiguration.Mode.Mode3);
var spiBus = Device.CreateSpiBus(
clock: Device.Pins.SCK,
copi: Device.Pins.MOSI,
cipo: Device.Pins.MISO,
config: config);
var st7789 = new St7789
(
device: Device,
spiBus: spiBus,
chipSelectPin: Device.Pins.D02,
dcPin: Device.Pins.D01,
resetPin: Device.Pins.D00,
width: 240, height: 240
);
displayWidth = Convert.ToInt32(st7789.Width);
displayHeight = Convert.ToInt32(st7789.Height);
graphics = new MicroGraphics(st7789);
graphics.Rotation = RotationType._270Degrees;
onboardLed.SetColor(Color.Green);
}
void DrawShapes()
{
Random rand = new Random();
graphics.Clear(true);
int radius = 10;
int originX = displayWidth / 2;
int originY = displayHeight / 2;
for (int i=1; i<5; i++)
{
graphics.DrawCircle
(
centerX: originX,
centerY: originY,
radius: radius,
color: Color.FromRgb(
rand.Next(128,255), rand.Next(128, 255), rand.Next(128, 255))
);
graphics.Show();
radius += 30;
}
int sideLength = 30;
for (int i = 1; i < 5; i++)
{
graphics.DrawRectangle
(
x: (displayWidth - sideLength) / 2,
y: (displayHeight - sideLength) / 2,
width: sideLength,
height: sideLength,
color: Color.FromRgb(
rand.Next(128, 255), rand.Next(128, 255), rand.Next(128, 255))
);
graphics.Show();
sideLength += 60;
}
graphics.DrawLine(0, displayHeight / 2, displayWidth, displayHeight / 2,
Color.FromRgb(
rand.Next(128, 255), rand.Next(128, 255), rand.Next(128, 255)));
graphics.DrawLine(displayWidth / 2, 0, displayWidth / 2, displayHeight,
Color.FromRgb(
rand.Next(128, 255), rand.Next(128, 255), rand.Next(128, 255)));
graphics.DrawLine(0, 0, displayWidth, displayHeight,
Color.FromRgb(
rand.Next(128, 255), rand.Next(128, 255), rand.Next(128, 255)));
graphics.DrawLine(0, displayHeight, displayWidth, 0,
Color.FromRgb(
rand.Next(128, 255), rand.Next(128, 255), rand.Next(128, 255)));
graphics.Show();
//Thread.Sleep(5000);
}
}
First thing that happens in this class is initialize the ST7789 display connected to Meadow and when creating a GraphicLibrary object, note that we pass the display object. This links the graphics library to the display we're using.
After Initializing everything, at the end of the constructor it calls the method DrawShapes, where you can see all the API methods that graphics
offers to draw shapes and lines, specifying colors, filled or empty, sizes, etc. It's important to note that when you want to clear the screen you call graphics.Clear(true);
and when you want to display everything you have in the buffer you call graphics.Show();
Note: Make sure when drawing shapes you don't exceed the screen size, otherwise you'll get an OutOfBoundsException
.
Run the project
Click the Run button in Visual Studio to see your shapes in the LCD display! Feel free to play around with more colors and sizes to have a It should look like to the following gif:
Now in the following code example you'll see how to display text of different font sizes. In your MeadowApp class, add the following code or replace it altogether:
void DrawTexts()
{
graphics.Clear(true);
int indent = 20;
int spacing = 20;
int y = 5;
graphics.CurrentFont = new Font12x16();
graphics.DrawText(indent, y, "Meadow F7 SPI ST7789!!");
graphics.DrawText(indent, y += spacing, "Red", Color.Red);
graphics.DrawText(indent, y += spacing, "Purple", Color.Purple);
graphics.DrawText(indent, y += spacing, "BlueViolet", Color.BlueViolet);
graphics.DrawText(indent, y += spacing, "Blue", Color.Blue);
graphics.DrawText(indent, y += spacing, "Cyan", Color.Cyan);
graphics.DrawText(indent, y += spacing, "LawnGreen", Color.LawnGreen);
graphics.DrawText(indent, y += spacing, "GreenYellow", Color.GreenYellow);
graphics.DrawText(indent, y += spacing, "Yellow", Color.Yellow);
graphics.DrawText(indent, y += spacing, "Orange", Color.Orange);
graphics.DrawText(indent, y += spacing, "Brown", Color.Brown);
graphics.Show();
Thread.Sleep(5000);
}
Inside DrawTexts, notice that before calling the DrawText method, we specify the font size in the CurrentFont property of graphics
. Next we have a series of DrawText calls, where you have to specify the x and y coordinates along with the text string and specify a Color if the display supports it.
Note: The current version of DisplayGraphics doesn't support text wrapping, so make sure the text you want to display fits in your screen, otherwise you'll get an OutOfBoundsException
.
Run the project
Click the Run button in Visual Studio to see your text samples in the display! Notice how it can also display certain especial characters. It should look like to the following gif:
Now lets combine what we've learned so far and make a watch face as an example. Replace the entire MeadowApp class code with the following, and we'll go over the logic below:
void DrawClock()
{
graphics.Clear(true);
hour = 8;
minute = 54;
DrawWatchFace();
while (true)
{
tick++;
Thread.Sleep(1000);
UpdateClock(second: tick % 60);
}
}
void DrawWatchFace()
{
graphics.Clear();
int hour = 12;
int xCenter = displayWidth / 2;
int yCenter = displayHeight / 2;
int x, y;
graphics.DrawRectangle(0, 0, displayWidth, displayHeight, Color.White);
graphics.DrawRectangle(5, 5, displayWidth - 10, displayHeight - 10, Color.White);
graphics.CurrentFont = new Font12x20();
graphics.DrawCircle(xCenter, yCenter, 100, WatchBackgroundColor, true);
for (int i = 0; i < 60; i++)
{
x = (int)(xCenter + 80 * Math.Sin(i * Math.PI / 30));
y = (int)(yCenter - 80 * Math.Cos(i * Math.PI / 30));
if (i % 5 == 0)
{
graphics.DrawText(hour > 9? x-10 : x-5, y-5, hour.ToString(), Color.Black);
if (hour == 12) hour = 1; else hour++;
}
}
graphics.Show();
}
void UpdateClock(int second = 0)
{
int xCenter = displayWidth / 2;
int yCenter = displayHeight / 2;
int x, y, xT, yT;
if (second == 0)
{
minute++;
if (minute == 60)
{
minute = 0;
hour++;
if (hour == 12)
{
hour = 0;
}
}
}
graphics.Stroke = 3;
//remove previous hour
int previousHour = (hour - 1) < -1 ? 11 : (hour - 1);
x = (int)(xCenter + 43 * Math.Sin(previousHour * Math.PI / 6));
y = (int)(yCenter - 43 * Math.Cos(previousHour * Math.PI / 6));
xT = (int)(xCenter + 3 * Math.Sin((previousHour - 3) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((previousHour - 3) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);
xT = (int)(xCenter + 3 * Math.Sin((previousHour + 3) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((previousHour + 3) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);
//current hour
x = (int)(xCenter + 43 * Math.Sin(hour * Math.PI / 6));
y = (int)(yCenter - 43 * Math.Cos(hour * Math.PI / 6));
xT = (int)(xCenter + 3 * Math.Sin((hour - 3) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((hour - 3) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, Color.Black);
xT = (int)(xCenter + 3 * Math.Sin((hour + 3) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((hour + 3) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, Color.Black);
//remove previous minute
int previousMinute = minute - 1 < -1 ? 59 : (minute - 1);
x = (int)(xCenter + 55 * Math.Sin(previousMinute * Math.PI / 30));
y = (int)(yCenter - 55 * Math.Cos(previousMinute * Math.PI / 30));
xT = (int)(xCenter + 3 * Math.Sin((previousMinute - 15) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((previousMinute - 15) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);
xT = (int)(xCenter + 3 * Math.Sin((previousMinute + 15) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((previousMinute + 15) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);
//current minute
x = (int)(xCenter + 55 * Math.Sin(minute * Math.PI / 30));
y = (int)(yCenter - 55 * Math.Cos(minute * Math.PI / 30));
xT = (int)(xCenter + 3 * Math.Sin((minute - 15) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((minute - 15) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, Color.Black);
xT = (int)(xCenter + 3 * Math.Sin((minute + 15) * Math.PI / 6));
yT = (int)(yCenter - 3 * Math.Cos((minute + 15) * Math.PI / 6));
graphics.DrawLine(xT, yT, x, y, Color.Black);
//remove previous second
int previousSecond = second - 1 < -1 ? 59 : (second - 1);
x = (int)(xCenter + 70 * Math.Sin(previousSecond * Math.PI / 30));
y = (int)(yCenter - 70 * Math.Cos(previousSecond * Math.PI / 30));
graphics.DrawLine(xCenter, yCenter, x, y, WatchBackgroundColor);
//current second
x = (int)(xCenter + 70 * Math.Sin(second * Math.PI / 30));
y = (int)(yCenter - 70 * Math.Cos(second * Math.PI / 30));
graphics.DrawLine(xCenter, yCenter, x, y, Color.Red);
graphics.Show();
}
Lets break down the explanation, starting from the DrawClock method that's invoked at the end of MeadowApp's constructor:
- DrawClock is in charge of initialize the initial hour and minutes of the time, calls DrawWatchFace, and enters into a infinite while loop that in every 1000ms (or 1s) calls UpdateClock passing the remainder operator of ticks divided by 60.
- DrawWatchFace method is called once, and it draws the watch's background, hour numbers.
- UpdateClock displays the clock's hour, minute and second hand, and its invoked every second to update the time like any regular clock. Notice that before drawing the new arm's position, we are re-drawing the arms current position with the same color as the background. We do this to update all the hands positions without having to clear and redraw the entire display in each iteration.
Run the project
Click the Run button in Visual Studio to see your clock in action! It should look like to the following gif:
Note that the time on the clock will be displayed based on the initial hour and minute values set in the DrawClock method. You could wire an RTC module to keep the time regardless if Meadow loses power, or use push buttons to adjust the time anytime you need to.
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.
Comments