Project updated to V1.0 Release Candidate 1 (October 23rd, 2022)
In this project we're making a plant monitor to keep a plant nice and healthy using a capacitive soil moisture sensor, an analog temperature sensor and a ST7789 display to show the status of the plant. We can drive all these peripherals using Meadow.Foundation, and we'll see how easy is to use all their APIs and write the logic in C#.
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.
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 circuitWire your project like this:
Create a new Meadow Application project in Visual Studio 2019 for Windows or macOS and name it PlantMonitor.
Step 3 - Add the required NuGet packagesFor this project, search and install the following NuGet packages:
Step 4 - Add Plant's health image assetsTo indicate how your plant's water level is doing, lets add this cute plant illustration to our project.
Download the image assets from the project GitHub repo, and add them to your project.
Important Note: Ensure the Build action is set to Embedded resource. Right-click on every image and click on Properties to set this.
Lets go over each class to build this project:
DisplayController class
Add a new Class and name it DisplayController. This method is used to encapsulate all the logic of graphics and display the image and text on the display.
public class DisplayController
{
MicroGraphics graphics;
public DisplayController(St7789 display)
{
graphics = new MicroGraphics(display);
graphics.CurrentFont = new Font12x20();
graphics.Stroke = 3;
graphics.Clear();
graphics.DrawRectangle(0, 0, 240, 240, Color.White, true);
string plant = "Plant";
string monitor = "Monitor";
graphics.CurrentFont = new Font12x16();
graphics.DrawText((240 - (plant.Length * 24)) / 2, 80, plant, Color.Black, ScaleFactor.X2);
graphics.DrawText((240 - (monitor.Length * 24)) / 2, 130, monitor, Color.Black, ScaleFactor.X2);
graphics.Show();
}
void UpdateImage(int index, int xOffSet, int yOffSet)
{
var jpgData = LoadResource($"level_{index}.jpg");
var decoder = new JpegDecoder();
var jpg = decoder.DecodeJpeg(jpgData);
int x = 0;
int y = 0;
byte r, g, b;
graphics.DrawRectangle(0, 0, 240, 208, Color.White, true);
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;
}
}
graphics.Show();
}
byte[] LoadResource(string filename)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"PlantMonitor.{filename}";
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
return ms.ToArray();
}
}
}
public void UpdateMoistureImage(double moistureReadings)
{
double moisture = moistureReadings;
if (moisture > 1) moisture = 1f;
else if (moisture < 0) moisture = 0f;
if (moisture > 0 && moisture <= 0.25) {
UpdateImage(0, 42, 10);
}
else if (moisture > 0.25 && moisture <= 0.50) {
UpdateImage(1, 28, 4);
}
else if (moisture > 0.50 && moisture <= 0.75) {
UpdateImage(2, 31, 5);
}
else if (moisture > 0.75 && moisture <= 1.0) {
UpdateImage(3, 35, 5);
}
graphics.Show();
}
public void UpdateMoisturePercentage(double newValue, double oldValue)
{
if (newValue > 1) newValue = 1f;
else if (newValue < 0) newValue = 0f;
graphics.DrawText(0, 208, $"{(int)(oldValue * 100)}%", Color.White, ScaleFactor.X2);
graphics.DrawText(0, 208, $"{(int)(newValue * 100)}%", Color.Black, ScaleFactor.X2);
graphics.Show();
}
public void UpdateTemperatureValue(Temperature newValue, Temperature oldValue)
{
string t = $"{(int)oldValue.Celsius}C";
graphics.DrawText(240 - t.Length * 24, 208, t, Color.White, ScaleFactor.X2);
t = $"{(int)newValue.Celsius}C";
graphics.DrawText(240 - t.Length * 24, 208, t, Color.Black, ScaleFactor.X2);
graphics.Show();
}
}
Lets break down this class by the methods:
- Contructor - In the constructor we're creating a GraphicsLibrary object by passing the St7789 display objetc from the main MeadowApp class. We then clear the display and draw the words Plant Monitor in the middle of the screen by passing the X and Y coordinates and setting the Font to be 12x20 pixels with a scale factor X2 so the text is displayed double the size.
- UpdateImage - this method uses the
SimpleJpegDecoder
to decode the array of bytes returned fromLoadResource
method, which are assigned to thejpg
byte array that has all the red, green and blue values on each pixel on after another. - LoadResource - Opens the image fileto get the stream and returns an array of bytes of raw data.
- UpdateMoistureImage - Receives the soil moisture values from the sensor, and based on the moisture level value, it calls UpdateImage to load and display the corresponding image to reflect the plant's water level.
- UpdateMoisturePercentage - Updates the moisture percentage value on the display. The text is drawn in the bottom left side of the screen.
- UpdateTemperatureValue - Updates the room temperature value on the display. The text is drawn in the bottom right side of the screen.
MeadowApp class
Copy the following code below:
// public class MeadowApp : App<F7FeatherV1> <- If you have a Meadow F7v1.*
public class MeadowApp : App<F7FeatherV2>
{
readonly Voltage MINIMUM_VOLTAGE_CALIBRATION = new Voltage(2.81, VU.Volts);
readonly Voltage MAXIMUM_VOLTAGE_CALIBRATION = new Voltage(1.50, VU.Volts);
double moisture;
Temperature temperature;
RgbPwmLed onboardLed;
PushButton button;
Capacitive capacitive;
AnalogTemperature analogTemperature;
DisplayController displayController;
public override Task Initialize()
{
onboardLed = new RgbPwmLed(
device: Device,
redPwmPin: Device.Pins.OnboardLedRed,
greenPwmPin: Device.Pins.OnboardLedGreen,
bluePwmPin: Device.Pins.OnboardLedBlue);
onboardLed.SetColor(Color.Red);
button = new PushButton(Device, Device.Pins.D04, ResistorMode.InternalPullUp);
button.Clicked += ButtonClicked;
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 display = new St7789
(
device: Device,
spiBus: spiBus,
chipSelectPin: Device.Pins.D02,
dcPin: Device.Pins.D01,
resetPin: Device.Pins.D00,
width: 240, height: 240
);
displayController = new DisplayController(display);
capacitive = new Capacitive(
device: Device,
analogInputPin: Device.Pins.A01,
minimumVoltageCalibration: MINIMUM_VOLTAGE_CALIBRATION,
maximumVoltageCalibration: MAXIMUM_VOLTAGE_CALIBRATION);
var capacitiveObserver = Capacitive.CreateObserver(
handler: result =>
{
onboardLed.SetColor(Color.Purple);
displayController.UpdateMoistureImage(result.New);
displayController.UpdateMoisturePercentage(result.New, result.Old.Value);
onboardLed.SetColor(Color.Green);
},
filter: null
);
capacitive.Subscribe(capacitiveObserver);
capacitive.StartUpdating(TimeSpan.FromHours(1));
analogTemperature = new AnalogTemperature(Device, Device.Pins.A00, AnalogTemperature.KnownSensorType.LM35);
var analogTemperatureObserver = AnalogTemperature.CreateObserver(
handler =>
{
onboardLed.SetColor(Color.Purple);
displayController.UpdateTemperatureValue(handler.New, handler.Old.Value);
onboardLed.SetColor(Color.Green);
},
filter: null
);
analogTemperature.Subscribe(analogTemperatureObserver);
analogTemperature.StartUpdating(TimeSpan.FromHours(1));
onboardLed.SetColor(Color.Green);
return base.Initialize();
}
async void ButtonClicked(object sender, EventArgs e)
{
onboardLed.SetColor(Color.Orange);
var newMoisture = await capacitive.Read();
var newTemperature = await analogTemperature.Read();
displayController.UpdateMoisturePercentage(newMoisture, moisture);
moisture = newMoisture;
displayController.UpdateTemperatureValue(newTemperature, temperature);
temperature = newTemperature;
onboardLed.SetColor(Color.Green);
}
}
In this main class project, we have several peripherals:
- RgbPwmLed onboardLed - as the name suggest this driver is used to control the onboard RGB LED on the Meadow board so it can give us feedback for when its doing certain things like initializing, its reading from sensors, etc.
- PushButton button - we'll use this button to trigger an event when its pushed and we activate the soil moisture and temperature sensors and refresh its values on the display. In the ButtonClicked event handler, notice the onboard LED turns orange as its reading values from the sensor, calls the corresponding methods of the DisplayController to update the values on the screen, and the LED turns green to indicate that the method has finished.
- Capacitive capacitive - this is the soil moisture sensor. We first create an object specifying to which Analog pin is connected to along with calibration values previously found specifically for my plant's soil. We then use the IObservable/Reactive Pattern so we can Subscribe for sensor changes with delta values of 0.5 so the app is not constantly reading and updating the values for every minor change. Finally we call StartUpdating to specify the sampleCount, sampleIntervalDuration and the standbyDuration so the sensor is active every hour.
- AnalogTemperature analogTemperature - we'll be using the LM35 analog temperature sensor to show the room temperature on the display. In the Initialize method we create the object and also use the IObservable/Reactive pattern, this time with a filter of one degree Celsius. Every hour when the sensor is activated, if the temperature has risen or lowered by at least 1 degree, it will call the DisplayController's UpdateTemperatureValue method to update the value on the display.
- DisplayController displayController - we'll use this object to load and draw images and text to our display. In the Initialize method, notice we first create a St7789 display object, and we pass that in to the displayController so it creates the GraphicsLibrary object that takes care of the rest on drawing images and text on the screen.
Place the soil moisture sensor in your plant, run the project and It should look like:
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