Back to overview
Raspberry Pi Waterer
Raspberry Pi Plant Waterer
April 23, 2020

Overview

Due to coronavirus, I have come back home from college. I have been keeping busy by doing some more self-learning and tinkering with electronics. I had the materials lying around to create a raspberry pi powered plant watering system, so I created one! I wanted the pi to send data to a webserver to live update stats on the plant's vitals, temperature, and moisture. So here we go!

This was by no means cheap, but the project nevertheless was enjoyable to make.

Materials:

Raspberry Pi 35
Pump 25
Soil Sensor 7.50
N-channel MOSFET 1.75
12V Power Supply 9
DC Power Adapter 2
Total 80.25

Extras (not strictly necessary):

JST Socket Cable 1.5
Wires Varies

The Pi

Circuit

I am using an N-channel MOSFET to control the flow of electricity to the pump. The pump is on a 12V line, and the raspberry pi is on a 5V line. If I powered the raspberry with the pumps power supply I would fry the raspberry pi (which I have done by miss wiring the MOSFET). The orange line is connected to the gpio pin for the pump (this can be whatever, in my case, it is gpio pin 14). This turns on/off the pump by allowing the pump's circuit to be complete, allowing for current to flow.

If you are using a different IO device, make sure the the soil sensor is powered by the default voltage of the device.

The Code

Please take a look at the full code repo here.

Enable SSH/SPI/I2C

To connect to the soil sensor we need to enable SPI and I2C. I also recommend enabling SSH to connect to the raspberry pi in the future. To enable these feature run:

sudo raspi-config

It will open like this:

Open the "Interfacing Options" window.

Enable the option, do this for the rest

Python Dependencies

Requirements:

# Update/Upgrade
sudo apt update
sudo apt upgrade
# python3 tools
sudo apt install python3 python3-pip python3
# gpiozero
sudo apt install python3-gpiozero
# Stemma Soil Sensor
sudo pip3 install adafruit-circuitpython-seesaw
# Zeromq
sudo pip3 install pyzmq

TL;DR

sudo apt update && sudo apt upgrade -y && sudo apt install -y python3 python3-pip python3 python3-gpiozero && sudo pip3 install adafruit-circuitpython-seesaw pyzmq
Python Code
import time
# for pump
from gpiozero import LED
# for soil
import busio
from board import SCL, SDA
from adafruit_seesaw.seesaw import Seesaw
import zmq

# zeromq publisher/subscriber pattern
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://*:5555")


# i2c for soil sensor
i2c_bus = busio.I2C(SCL, SDA)
ss = Seesaw(i2c_bus, addr=0x36)

# pump
pump = LED(14)

while True:
    # read moisture level through capacitive touch pad
    touch = ss.moisture_read()
    # read temperature from the temperature sensor
    temp = ss.get_temp()
    # turn on/off pump
    if touch < 500:
        pump.on()
    else:
        pump.off()
    print("Temperature C: {} Moisture: {}".format(temp, touch))
    # send data with publisher/subscriber pattern with zeromq
    socket.send_json({'temperature': temp, 'moisture': touch})
    time.sleep(1)

To enable the soil python script on boot I am using a systemd service, which is simple and clean, and cron jobs are kind of pain.

[Unit]
Description=Soil Waterer
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User=pi
ExecStart=/usr/bin/env python3 /home/pi/soil.py

[Install]
WantedBy=multi-user.target

Once the system file is created you should to enable it to run on boot:

sudo systemctl enable soil

To start it immediatly run:

sudo systemctl start soil

The Client/Webserver

I wanted a way to get live stats from the sensor, so I thought about various forms of sending data. I tried using UNIX sockets to send data from the Python script to a nodejs express web server, but that was very flimsy. I then settled on using zeromq, which is absolutely beautiful. The way it is structured, the python script creates a publisher/subscriber pattern and sends data to any subscriber. Any subscriber can tap into that data and use it in their application. I then created an Electron Desktop application with ReactJS. Electron is needed since zeromq requires nodejs libraries and makes building a GUI very easy.

A Simple Test

To make sure I was actually getting the zeromq data, I created a simple script on a different computer to see if I was getting the data (based on nodejs zeromq readme). It worked perfectly!

import zmq from 'zeromq';
const sock = zmq.socket('sub'); // subscriber

sock.connect('tcp://IP_ADDRESS:5555');
sock.subscribe(''); // No topic
console.log('Connected');

sock.on('message', (message) => {
  // Convert message to JSON
  const json = JSON.parse(message.toString());
  console.log(json);
});

Creating Electron React App

Setuping up Electron with React is simple, but I always forget how. I often find myself following Randy Findley's guide which I am going to summarize below (please read his).

Install

So first create the react app with create-react-app. I wanted to use TypeScript and yarn so I included those options.

npx create-react-app soil-electron --template typescript --use-yarn

Then in soil-electron we have to add electron.

yarn add --dev electron electron-builder

And then some development tools.

yarn add electron-is-dev
yarn add --dev wait-on concurrently

Then we need to create public/electron.js to bootstrap electron.

const { app, BrowserWindow } = require('electron');
const path = require('path');
const isDev = require('electron-is-dev');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({ width: 900, height: 680 });
  mainWindow.loadURL(
    isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`
  );
  if (isDev) {
    // Open the DevTools.
    //BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
    mainWindow.webContents.openDevTools();
  }
  mainWindow.on('closed', () => (mainWindow = null));
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});
Using Rescripts to Get Access to IPC (interprocess communication)

Then to use electron nodejs based libraries we need to install and setup rescripts.

yarn add --dev @rescripts/cli @rescripts/rescript-env

Then create .rescriptsrc.js

module.exports = [require.resolve('./.webpack.config.js')];

And create .webpack.config.js

module.exports = (config) => {
  config.target = 'electron-renderer';
  return config;
};

Finally, update the scripts of package.json to use rescripts instead of the default react-scripts


{
  // rest of package.json
  "scripts": {
    // other scripts
    "start": "rescripts start",
    "build": "rescripts build",
    "test": "rescripts test"
  }
}
Fixes for zeromq

I had issues building the electron binary with zeromq and found that I need to rebuild electron with zeromq. To do so install electron-rebuild:

yarn add electron-rebuild

And then add the script, to package.json to run on package install (aka yarn install).

{
  // rest of package.json
  "scripts": {
    // other scripts
    "rebuild": "electron-rebuild",
    "install": "yarn rebuild"
  }
}
IPC Setup

I used electron-better-ipc to better ipc with the react frontend and electron backend. Here's the updated code for the electron code (public/electron.js).

const electron = require('electron');
const { ipcMain: ipc } = require('electron-better-ipc');
const { app, BrowserWindow } = electron;
const path = require('path');
const isDev = require('electron-is-dev');
const zmq = require('zeromq');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 900,
    height: 680,
    webPreferences: { nodeIntegration: true }
  });
  mainWindow.loadURL(
    isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`
  );
  if (isDev) {
    // Open the DevTools.
    //BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
    mainWindow.webContents.openDevTools();
  }
  mainWindow.on('closed', () => (mainWindow = null));
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});

const sock = zmq.socket('sub');
sock.connect('tcp://192.168.0.107:5555');
sock.subscribe(''); // python zeromq has no topic option
console.log('Subscriber connected to port 5555');

let data;

sock.on('message', (zeromqData) => {
  data = JSON.parse(zeromqData.toString());
  console.log(data);
});

setInterval(() => {
  ipc.callRenderer(mainWindow, 'data', data);
}, 1000);
Frontend Code

Then I connected the frontend react code to electron to update some text and a graph. I am using rebass for styling and recharts for graphing.

import React, { useEffect, useState } from 'react';
import { Box, Heading, Flex, Text } from 'rebass';
import { LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Line } from 'recharts';
const { ipcRenderer: ipc } = window.require('electron-better-ipc');

function toPrecision(number: number, precision: number) {
  return Math.floor(Math.pow(10, precision) * number) / Math.pow(10, precision);
}

function App() {
  const [temp, setTemp] = useState<{ time: number; temperature: number }[]>([]);
  const [data, setData] = useState({ temperature: 0, moisture: 0 });

  useEffect(() => {
    const removeListener = ipc.answerMain(
      'data',
      (data: { temperature: number; moisture: number }) => {
        setData(data);
        setTemp((temp) =>
          [
            ...temp,
            {
              time: temp.length > 0 ? temp[temp.length - 1].time + 1 : 1,
              temperature: toPrecision((data.temperature * 212) / 100 + 32, 2)
            }
          ].slice(Math.max(0, temp.length - 10))
        );
      }
    );
    return removeListener;
  }, []);

  return (
    <Flex mx="auto" p={3}>
      <Box width="80%" sx={{ maxWidth: 1250 }}>
        <Heading fontSize={5}>Kyle's Plant System</Heading>
        <Text>Thanks to zeromq!</Text>
        <Heading>Temperature: {toPrecision((data.temperature * 212) / 100 + 32, 2)} °F</Heading>
        <Heading>Moisture: {toPrecision((data.moisture / 1015) * 100, 2)}%</Heading>
        <Box ml={4}>
          <LineChart
            width={730}
            height={250}
            data={temp}
            min
            margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
          >
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="time" />
            <YAxis domain={[70, 'auto']} />
            <Tooltip />
            <Legend />
            <Line type="monotone" dataKey="temperature" stroke="#8884d8" />
          </LineChart>
        </Box>
      </Box>
    </Flex>
  );
}

export default App;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { ThemeProvider } from 'theme-ui';
import preset from '@rebass/preset';

ReactDOM.render(
  <React.StrictMode>
    <ThemeProvider theme={preset}>
      <App />
    </ThemeProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

serviceWorker.unregister();

Final Product

And there you go! Now you have a working soil sensor that can send data to clients and turn on the pump when needed! I plan on making it better by adjusting the pump start moisture and allowing for clients to change that trigger via the application.