Wiring Up Pi-Lot

Posted on 2020-04-12

Wiring up pi-lot.

Ok. So I’ve had a go with each device separately. Let’s put it all together.

Components

  • Raspberry Pi Zero W.

  • DS18B20 - temperature probe

  • DHT22 - air temperature and humidity sensor

  • MCP3008 - analogue to digital converter

  • photoresistor

  • capacitive moisture sensor

  • water sensor

  • water pump

  • 2 small breadboards

  • some jump cables and solid core wire

  • A takeaway food box

  • garden wire

  • some plastic food containers

Physical arrangement

A takeaway container is used to house the RPi and two breadboards, held in place by garden wire. Holes have been made to accommodate cables and wire providing fixtures.

Circuits

Full schematic

Thanks KiCad. Way better than fritzing, which is great, but… “doesn’t scale” (or words to that effect).

Code

EDIT : I am finishing this retrospectively. Apparently it was all just too exciting at the time to keep up with my notes. I detoured via some very elaborate database structures and with async polling and whatnot. And in the end returned to KISS. Classic.

I hacked together the code used when experimenting with the various components independently into a single main.py. This abstracts the various devices into their respective classes, and Pilot which then handles them. Get it?! “pi-lot”/ “pilot”… clever ’ey.

Pilot collects the readings and logs them in a database. The database logic is in db.py (also below) and the super simple schema consists of a single table “event”.

The water pump is not included after reading that automatic watering can be a killer. Believable.

""" main.py """ 
import time 
import collections
import glob

from spidev import SpiDev
import Adafruit_DHT
 
import db

class Pilot:
    """
    Main daemon of pilot sensors and instruments. 
    """

    def __init__(self, devices):
        self._devices = devices
        self._session_maker = db.session_maker()


    def logging(self, interval=1):
        def log_result(result):
            event = db.Event(category=result[0],state=result[1])
            session.add(event)
            session.commit()
        session = self._session_maker()
        try:
            while True:
                if db.get_size() > 3e9: #~3gb
                    raise IOError("DB quite big")
                for device in self._devices:
                    try:
                        for result in device.poll():
                            log_result(result)
                    except Exception as E:
                        print(device, E) 
                time.sleep(interval)
        except KeyboardInterupt: 
            print("Interupt") 
        finally: 
            print("Exiting") 
            session.close()

class MCP3008:
    def __init__(self, sensors=[], bus = 0, device = 0,):
        self._bus, self._device = bus, device
        self.spi = SpiDev()
        self.open()
        self._sensors = sensors + [None]*(8 - len(sensors))
 
    def open(self):
        self.spi.open(self._bus, self._device)
        self.spi.max_speed_hz = 1350000
    
    def poll_channel(self, channel=0):
        adc = self.spi.xfer2([1, (8 + channel) << 4, 0])
        data = ((adc[1] & 3) << 8) + adc[2]
        return data

    def poll(self):
        results = []
        for channel, sensor in enumerate(self._sensors):
            if sensor:
                results.append((sensor, self.poll_channel(channel)))
        return results

    def close(self):
        self.spi.close()

class DS18B20:
    def __init__(self):
        pass

    def read_temp_raw(self):
        base_dir = '/sys/bus/w1/devices/'
        device_folder = glob.glob(base_dir + '28*')[0]
        device_file = device_folder + '/w1_slave'
        with open(device_file, 'r') as fh:
            lines = fh.readlines()
        return lines

    def poll(self):
        lines = self.read_temp_raw()
        while lines[0].strip()[-3:] != 'YES':
            time.sleep(0.2)
            lines = self.read_temp_raw()
        equals_pos = lines[1].find('t=')
        if equals_pos != -1:
            temp_string = lines[1][equals_pos+2:]
            temp_c = float(temp_string) / 1000.0
            return [("soil_temp", temp_c)] #, temp_f

class DHT22:
    def __init__(self, pin=27):
        self._sensor = Adafruit_DHT.DHT22
        self._pin = pin # GPIO pin number used.

    def poll(self):
        while True:
            humidity, temperature = Adafruit_DHT.read_retry(self._sensor, self._pin)
            if humidity is not None and temperature is not None:
                return [("humidity", humidity), ("air_temp", temperature)]


if __name__ == "__main__":
    mcp = MCP3008(["moisture", "water_level", "photoresistor"])
    ds18b20 = DS18B20() 
    dht22 = DHT22()
    devices = [mcp, ds18b20, dht22]
    
    pilot = Pilot(devices) 
    pilot.logging(1) 

with db.py handling the database

"""db.py"""

import os 
import datetime

import sqlalchemy as sql
from sqlalchemy.ext.declarative import as_declarative

Base = sql.ext.declarative.declarative_base()

class Event(Base):
    __tablename__ = 'event'
    id = sql.Column(sql.Integer, primary_key=True)
    datetime = sql.Column(sql.DateTime, default=datetime.datetime.utcnow)
    category = sql.Column(sql.String)
    state = sql.Column(sql.Float) 

    def __repr__(self):
       return "<Event(datetime='{}', category='{}')>".format(self.datetime, self.category)


def create_db(engine):
    Base.metadata.create_all(engine)

def session_maker(name = "pilots_log.sqlite"):
    if not os.path.exists(name):
        engine = sql.create_engine('sqlite:///{}'.format(name), echo=True)
        create_db(engine)
    else:
        engine = sql.create_engine('sqlite:///{}'.format(name), echo=True)
    Session = sql.orm.sessionmaker(bind=engine)
    return Session

def get_size(name = "pilots_log.sqlite"):
    return os.stat(name).st_size
    

if __name__=="__main__":
    name = "pilots_log.sqlite"
    if not os.path.exists(name):
        engine = sql.create_engine('sqlite:///{}'.format(name), echo=True)
        create_db(engine)
    else:
        engine = sql.create_engine('sqlite:///{}'.format(name), echo=True)
    Session = sql.orm.sessionmaker(bind=engine)
    session = Session()
    session.close()

Setting this up on a pi, we can kick off the logging with

python main.py

(Using screen or tmux to detach and leave alive.)