Importing CircuitPython Libraries

Google is discontinuing support for Google Cloud IoT Core on August 16, 2023. On this date, existing PyPortal IoT Planters will not work. If you are attempting this project for the first time or need to migrate your project to another IoT Platform, we suggest following a guide for the AWS IoT Planter (https://learn.adafruit.com/pyportal-iot-plant-monitor-with-aws-iot-and-circuitpython), Azure IoT Planter (https://learn.adafruit.com/using-microsoft-azure-iot-with-circuitpython), or the Adafruit IO IoT Planter (https://learn.adafruit.com/pyportal-pet-planter-with-adafruit-io).
import time
import json
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
import board
import busio
import digitalio
import gcp_gfx_helper
import neopixel
from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager
from adafruit_gc_iot_core import MQTT_API, Cloud_Core
from adafruit_minimqtt import MQTT
from adafruit_seesaw.seesaw import Seesaw
from digitalio import DigitalInOut

The code first imports all of the modules required to run the code. Some of these libraries are CircuitPython core modules (they're "burned into" the firmware) and some of them you dragged into the library folder.

The code for this project imports a special adafruit_gc_iot_core library. To help simplify managing communication between your PyPortal and Google IoT Core's interfaces, we wrote a CircuitPython helper module called Adafruit_CircuitPython_GC_IOT_Core

We've also included a gcp_gfx_helper.py file which handles displaying the status of the code on the PyPortal's display.

Configuring the PyPortal's WiFi Hardware

The next chunk of code grabs information from a secrets.py file including wifi configuration. Then, it sets up the ESP32's SPI connections for use with the PyPortal. The wifi object is set up here too - it's used later in the code to communicate with the IoT Hub.

# Get wifi details and more from a secrets.py file
try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise

# PyPortal ESP32 Setup
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards
"""Uncomment below for ItsyBitsy M4"""
#status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2)
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)

Configuring the Soil Sensor and Water Pump

An I2C busio device is set up and linked to the soil sensor's address (0x36). The water pump is configured as a digitalio output, since you'll be controlling the pump using the "transistor switch" circuit.

# Soil Sensor Setup
i2c_bus = busio.I2C(board.SCL, board.SDA)
ss = Seesaw(i2c_bus, addr=0x36)

# Water Pump Setup
water_pump = digitalio.DigitalInOut(board.D3)
water_pump.direction = digitalio.Direction.OUTPUT

Configuring the Graphical Helper

The graphics helper, which manages' the PyPortal's display is created. If you wish to display the temperature in Fahrenheit instead of Celsius, add is_celsius=True to the method call.

gfx = gcp_gfx_helper.Google_GFX()

Connection Callback Methods

The following methods are used as MQTT client callbacks. They only execute when the broker (Google Cloud MQTT)  communicates with your PyPortal.

# Define callback methods which are called when events occur
def connect(client, userdata, flags, rc):
    # This function will be called when the client is connected
    # successfully to the broker.
    print('Connected to Google Cloud IoT!')
    print('Flags: {0}\nRC: {1}'.format(flags, rc))
    # Subscribes to commands/# topic
    google_mqtt.subscribe_to_all_commands()

def disconnect(client, userdata, rc):
    # This method is called when the client disconnects
    # from the broker.
    print('Disconnected from Google Cloud IoT!')

def subscribe(client, userdata, topic, granted_qos):
    # This method is called when the client subscribes to a new topic.
    print('Subscribed to {0} with QOS level {1}'.format(topic, granted_qos))


def unsubscribe(client, userdata, topic, pid):
    # This method is called when the client unsubscribes from a topic.
    print('Unsubscribed from {0} with PID {1}'.format(topic, pid))


def publish(client, userdata, topic, pid):
    # This method is called when the client publishes data to a topic.
    print('Published to {0} with PID {1}'.format(topic, pid))


def message(client, topic, msg):
    # This method is called when the client receives data from a topic.
    try:
        # Attempt to load a JSON command
        msg_dict = json.loads(msg)
        # Handle water-pump commands
        if msg_dict['pump_time']:
            handle_pump(msg_dict)
    except:
        # Non-JSON command, print normally
        print("Message from {}: {}".format(topic, msg))

Connecting to Cloud IoT Core

We created a helper class within the adafruit_gc_iot_core module to assist creating the project identifier and handling authentication. Cloud IoT Core's settings (Cloud_Core) are initialized. The JWT (JSON Web Token) used for authenticating with the server is generated for you automatically using the CircuitPython JWT module and the private RSA key you provided earlier.

# Initialize Google Cloud IoT Core interface
google_iot = Cloud_Core(esp, secrets)

# JSON-Web-Token (JWT) Generation
print("Generating JWT...")
jwt = google_iot.generate_jwt()
print("Your JWT is: ", jwt)

After the JWT has been successfully created, we'll set up a new MiniMQTT client for communicating with the Google MQTT API. This client uses a few variables from the google_iot object we created earlier, along with the JWT we just generated.

The code also initializes the Google MQTT API client, an interface which simplifies using MiniMQTT to communicate with Google Core IoT.

# Set up a new MiniMQTT Client
client = MQTT(socket,
              broker=google_iot.broker,
              username=google_iot.username,
              password=jwt,
              client_id=google_iot.cid,
              network_manager=wifi)

# Initialize Google MQTT API Client
google_mqtt = MQTT_API(client)

The connection callback methods created earlier are connected to the google_mqtt client and the code attempts to connect to Google Cloud IoT Core.

# Connect callback handlers to Google MQTT Client
google_mqtt.on_connect = connect
google_mqtt.on_disconnect = disconnect
google_mqtt.on_subscribe = subscribe
google_mqtt.on_unsubscribe = unsubscribe
google_mqtt.on_publish = publish
google_mqtt.on_message = message

print('Attempting to connect to %s' % client.broker)
google_mqtt.connect()

Once Google's MQTT broker successfully connects with your client, it'll call the connect() callback method. This method subscribes to the device's default commands topic (commands/#). Any data sent to this topic will be received by the code's message() callback.  

def connect(client, userdata, flags, rc):
    # This function will be called when the client is connected
    # successfully to the broker.
    print('Connected to Google Cloud IoT!')
    print('Flags: {0}\nRC: {1}'.format(flags, rc))
    # Subscribes to commands/# topic
    google_mqtt.subscribe_to_all_commands()

Main Loop

The main loop takes the current time and compares it to the desired SENSOR_DELAY time in minutes (set at the top of the code).

If the time has exceeded SENSOR_DELAY, the code reads the moisture level and temperature from the STEMMA soil sensor. Then, it displays the values of the soil sensor on the PyPortal using the gfx module. 

if now - initial > (SENSOR_DELAY * 60):
  # read moisture level
  moisture_level = ss.moisture_read()
  # read temperature
  temperature = ss.get_temp()
  # Display Soil Sensor values on pyportal
  temperature = gfx.show_temp(temperature)
  gfx.show_water_level(moisture_level)

Then the temperature and moisture_level are published to the device's default events topic. We added a two second delay between publishing to ensure we don't get throttled by Google's MQTT broker.

Then, the timer will set itself to the current time.monotonic value.

print('Sending data to GCP IoT Core')
gfx.show_gcp_status('Publishing data...')
google_mqtt.publish(temperature, "events")
time.sleep(2)
google_mqtt.publish(moisture_level, "events")
gfx.show_gcp_status('Data published!')
print('Data sent!')
# Reset timer
initial = now

If the SENSOR_DELAY time has not yet elapsed, we'll poll the Google MQTT broker to ensure we retain communication with the broker. google_mqtt.loop() pings Google's MQTT broker and listenings for a response back from it. It also queries the broker for any messages received (such as a message from Google Cloud IoT telling the pump to turn on). 

All of this code is wrapped inside a try/except control flow. If the WiFi module fails at any point, the program will execute the except and reset the module before going back to the top of the try.

Handling the Water Pump Messages

One interesting chunk of this code that we haven't yet discussed is How is data from google cloud received by the PyPortal?

Since the code is subscribed to all messages from the device/commands/# topic, every message will be received by the message() method. 

Since we're sending the PyPortal a message from Google Cloud in JSON format, we attempt to decode if the incoming message is a JSON dictionary. 

If it is a JSON command, the code looks for the pump_time key in the JSON dictionary. If it's found, the JSON dictionary is passed to a handle_pump method.

If it's not a JSON command, the code will simply print out the message and the topic it was received on. 

def message(client, topic, msg):
    # This method is called when the client receives data from a topic.
    try:
        # Attempt to decode a JSON command
        msg_dict = json.loads(msg)
        # Handle water-pump commands
        if msg_dict['pump_time']:
            handle_pump(msg_dict)
    except:
        # Non-JSON command, print normally
        print("Message from {}: {}".format(topic, msg))

The handle_pump method parses the pump_status and pump_time from the command's JSON dictionary.

If the pump command is enabling the pump, the code will print that it is starting the pump for pump_time and start a timer. While the timer is not expired, it'll turn on the pump by setting water_pump.value to True.

Once the timer expires, the screen will output that the plant is watered and the pump will be disabled.

def handle_pump(command):
    """Handles command about the planter's
    watering pump from Google Core IoT.
    :param json command: Message from device/commands#
    """
    print("handling pump...")
    # Parse the pump command message
    # Expected format: {"power": true, "pump_time":3}
    pump_time = command['pump_time']
    pump_status = command['power']
    if pump_status:
        print("Turning pump on for {} seconds...".format(pump_time))
        start_pump = time.monotonic()
        while True:
            gfx.show_gcp_status('Watering plant...')
            cur_time = time.monotonic()
            if cur_time - start_pump > pump_time:
                # Timer expired, leave the loop
                print("Plant watered!")
                break
            water_pump.value = True
    gfx.show_gcp_status('Plant watered!')
    print("Turning pump off")
    water_pump.value = False

This guide was first published on Aug 28, 2019. It was last updated on Aug 28, 2019.

This page (Code Walkthrough) was last updated on Aug 22, 2019.

Text editor powered by tinymce.