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 (, Azure IoT Planter (, or the Adafruit IO IoT Planter (

CircuitPython Library Installation

First make sure you are running the latest version of Adafruit CircuitPython for your board.

Next you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle matching your version of CircuitPython. PyPortal requires at least CircuitPython version 4.0.0.

Connect your PyPortal to your computer using a known good data+power USB cable. The board should show up in your operating system file explorer/finder as a flash drive names CIRCUITPY.

Before continuing make sure your board's lib folder on CIRCUITPY has the following files and folders copied over.

  • adafruit_binascii.mpy
  • adafruit_imageload
  • adafruit_bitmap_font
  • adafruit_itertools
  • adafruit_rsa
  • adafruit_bus_device
  • adafruit_jwt.mpy
  • adafruit_seesaw
  • adafruit_logging.mpy
  • neopixel.mpy
  • adafruit_esp32spi
  • adafruit_minimqtt
  • adafruit_gc_iot_core.mpy
  • adafruit_ntp.mpy
  • adafruit_register

Add CircuitPython Code and Project Assets

In the embedded code element below, click on the Download Project Bundle button, and save the .zip archive file to your computer.

Then, uncompress the .zip file, it will unpack to a folder named PyPortal_GC_IOT_CORE_PLANT_MONITOR.

Copy the contents of thePyPortal_GC_IOT_CORE_PLANT_MONITOR directory to your PyPortal CIRCUITPY drive.

# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries
# SPDX-License-Identifier: MIT

PyPortal Google Cloud IoT Core Planter
Water your plant remotely and log its vitals to Google
Cloud IoT Core with your PyPortal.

Author: Brent Rubell for Adafruit Industries, 2019
import time
import json
import board
import busio
import gcp_gfx_helper
import neopixel
import adafruit_connection_manager
from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager
from adafruit_gc_iot_core import MQTT_API, Cloud_Core
import adafruit_minimqtt.adafruit_minimqtt as MQTT
from adafruit_seesaw.seesaw import Seesaw
import digitalio

# Delay before reading the sensors, in minutes

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

# PyPortal ESP32 Setup
esp32_cs = digitalio.DigitalInOut(board.ESP_CS)
esp32_ready = digitalio.DigitalInOut(board.ESP_BUSY)
esp32_reset = digitalio.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)
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(
    esp, secrets, status_light)

# Connect to WiFi
print("Connecting to WiFi...")

pool = adafruit_connection_manager.get_radio_socketpool(esp)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp)

# 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

# Initialize the graphics helper
print("Loading GCP Graphics...")
gfx = gcp_gfx_helper.Google_GFX()
print("Graphics loaded!")

# Define callback methods which are called when events occur
# pylint: disable=unused-argument, redefined-outer-name
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

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.
        # Attempt to decode a JSON command
        msg_dict = json.loads(msg)
        # Handle water-pump commands
        if msg_dict['pump_time']:
    except TypeError:
        # Non-JSON command, print normally
        print("Message from {}: {}".format(topic, msg))

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!")
            water_pump.value = True
    gfx.show_gcp_status('Plant watered!')
    print("Turning pump off")
    water_pump.value = False

# 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)

# Set up a new MiniMQTT Client
client = MQTT.MQTT(,

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

# 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' %

# Time in seconds since power on
initial = time.monotonic()

while True:
        gfx.show_gcp_status('Listening for new messages...')
        now = time.monotonic()
        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)
            print('Sending data to GCP IoT Core')
            gfx.show_gcp_status('Publishing data...')
            google_mqtt.publish(temperature, "events")
            google_mqtt.publish(moisture_level, "events")
            gfx.show_gcp_status('Data published!')
            print('Data sent!')
            # Reset timer
            initial = now
    except (ValueError, RuntimeError, OSError, ConnectionError) as e:
        print("Failed to get data, retrying", e)

This is what the final contents of the CIRCUITPY drive will look like:


Install Mu Editor 

This guide requires you to edit and interact with CircuitPython code. While you can use any text editor of your choosing,  Mu is a simple code editor that works with the Adafruit CircuitPython boards. It's written in Python and works on Windows, MacOS, Linux and Raspberry Pi. The serial console is built right in, so you get immediate feedback from your board's serial output!

Before proceeding, click the button below to install the Mu Editor. There are versions for PC, mac, and Linux.

Secrets File Setup

Open the file on your CIRCUITPY drive using Mu. You're going to edit the file to enter your local WiFi credentials along with data about your Google Cloud Services Project and IoT Core configuration.

Make the following changes to the code below in the file:

  • Replace MY_WIFI_SSID with the name of your WiFi SSID
  • Replace MY_WIFI_PASSWORD with your WiFi's password
  • Replace MY_GCS_PROJECT_ID with the name of your Google Cloud Services project.
  • Replace MY_GCS_PROJECT_REGION with the project's region.
  • Replace MY_IOT_REGISTRY with the name of your Cloud IoT registry.
  • Replace MY_IOT_DEVICE with the name of the device you created in your Cloud IoT registry.
# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it
secrets = {
    'ssid' : 'MY_WIFI_SSID',
    'password' : 'MY_WIFI_PASSWORD',
    'project_id' : 'MY_GCS_PROJECT_ID',
    'cloud_region' : 'MY_GCS_PROJECT_REGION',
    'registry_id' : 'MY_IOT_REGISTRY',
    'device_id' : 'MY_IOT_DEVICE'

Authenticating with Google Cloud IoT Core - the JWT

CircuitPython authenticates with Google's MQTT API using a special security key called a JSON Web Token (JWT)

A JSON Web Token is a JSON dictionary containing statements (claims) about the device. In our case, it looks like the following:

token = {
  # The time that the token was issued at
  'iat': issue_time,
  # The time the token expires.
  'exp': expiration_time,
  # Your Project Identifier
  'aud': project_id

We are not going to create this JWT manually, the Google Core IoT CircuitPython module handles this automatically for you using the CircuitPython JWT module.

Creating a RSA Key Pair

The final step in generating a JWT involves signing the JWT with a private key. This ensures that the key will only be used to communicate with a public key.

Your CircuitPython device will hold the device's private RSA, while Google Cloud IoT holds the device's public RSA key

While the CircuitPython RSA module can generate RSA keys, it cannot save the key pairs to the device's filesystem. 

We'll be generating a RSA key pair on our computer using a Python script.

Click to download the code below and save the code to your desktop.

# SPDX-FileCopyrightText: 2019 Google Inc.
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries
# SPDX-License-Identifier: Apache-2.0


Generates RSA keys and decodes them using python-rsa
for use with a CircuitPython secrets file.

This script is designed to run on a computer,
NOT a CircuitPython device.

Requires Python-RSA (

* Author(s): Google Inc., Brent Rubell
import subprocess
import rsa

# Generate private and public RSA keys
with subprocess.Popen(["openssl", "genrsa", "-out", "rsa_private.pem", "2048"]) as proc:
with subprocess.Popen(
    ["openssl", "rsa", "-in", "rsa_private.pem", "-pubout", "-out", "rsa_public.pem"]
) as proc:

# Open generated private key file
    with open("rsa_private.pem", "rb") as file:
        private_key =
except:  # pylint: disable=bare-except
    print("No file named rsa_private.pem found in directory.")
pk = rsa.PrivateKey.load_pkcs1(private_key)

print("Copy and paste this into your file:\n")
print('"private_key": ' + str(pk)[10:] + ",")

You'll need a local installation of Python (click here to download and install the Python distribution for your operating system) to run this script.

You'll also need to install the python-rsa library, you can do this by typing the following into your terminal:

pip install rsa

Once installed, run the code from your desktop:


If the script runs successfully, it'll create two files on your desktop - rsa_public.pem and rsa_private.pem. These are your public and private RSA keys. You'll use them in the next step.

It'll also output the decoded private key to your terminal. The private key should have five integer values.

Modify your file by copying and pasting the output from the code into the file:

# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it

secrets = {
    'ssid' : 'MY_WIFI_SSID',
    'password' : 'MY_WIFI_PASSWORD',
    'project_id' : 'MY_GCS_PROJECT_ID',
    'cloud_region' : 'MY_GCS_PROJECT_REGION',
    'registry_id' : 'MY_IOT_REGISTRY',
    'device_id' : 'MY_IOT_DEVICE',
    "private_key": (24438159363269526254144311871580579031858357859966324445350326786364998912771634237547789185814277400858631944952065597018819979449812397621834604674421573629778503529017607686952918724898685881544477812184759676064843937596886704154729857293596401786101074754877589082423154083489847915661860834755738610786688547912322386416918350317006245900073735354143276892049027125601443947584374912401061688828446039255462953272156360234392950941978497936249124410101311599817221805182114152095007037371899964182199631346414794479580997760720063537930724219713985584071493296120508892403130706628712278713361122757185268631117,

Adding your RSA Public Key to Google Cloud IoT Core

After the private key is set up on your CircuitPython device, you'll also need to add the public key to the IoT Core Device. 

To view the public key data, enter the following into your terminal:

less rsa_public.pem

It should print the RSA Public Key data to your terminal.

Copy the output starting with -----BEGIN PUBLIC KEY----- to your clipboard.

Navigate to the IoT Core Device details page. Under Authenticationclick Add public key.

Specify the public key format to be RS256 and paste the public key data into the Public key value text box.

Click Add Public Key to add the authentication key to your device.

With the private key stored on your CircuitPython device and the public key stored in Google IoT Core, you're ready to run your code!

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

This page (Code Setup) was last updated on Apr 22, 2024.

Text editor powered by tinymce.