Code Walkthrough

Importing CircuitPython Libraries

Download: file
import time
import json
import board
import busio
from digitalio import DigitalInOut
import neopixel
from adafruit_esp32spi import adafruit_esp32spi
from adafruit_esp32spi import adafruit_esp32spi_wifimanager
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
from adafruit_minimqtt import MQTT
from adafruit_aws_iot import MQTT_CLIENT
from adafruit_seesaw.seesaw import Seesaw
import aws_gfx_helper

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 (lib on the PyPortal's CIRCUITPY drive).

The code for this project imports a special adafruit_aws_iot library. To help simplify managing communication between your PyPortal and AWS IoT's MQTT API, we wrote a CircuitPython helper module called Adafruit_CircuitPython_AWS_IOT

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

Configuring the PyPortal's ESP32

The next chunk of code grabs information from a secrets.py file about your WiFi AP configuration, AWS device identifier and AWS IoT endpoint. The device certificate and RSA private key are read into variables, DEVICE_CERT and DEVICE_KEY.

Download: file
# 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

# Get device certificate
try:
    with open("aws_cert.pem.crt", "rb") as f:
        DEVICE_CERT = f.read()
except ImportError:
    print("Certificate (aws_cert.pem.crt) not found on CIRCUITPY filesystem.")
    raise

# Get device private key
try:
    with open("private.pem.key", "rb") as f:
        DEVICE_KEY = f.read()
except ImportError:
    print("Key (private.pem.key) not found on CIRCUITPY filesystem.")
    raise

Then, it sets up the ESP32's SPI connections for use with the PyPortal along with a wifi manager for interfacing with the ESP32.

Download: file
# If you are using a board with pre-defined ESP32 Pins:
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)

# If you have an externally connected ESP32:
# esp32_cs = DigitalInOut(board.D9)
# esp32_ready = DigitalInOut(board.D10)
# esp32_reset = DigitalInOut(board.D5)

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)

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.

Download: file
# Initialize the graphics helper
print("Loading AWS IoT Graphics...")
gfx = aws_gfx_helper.AWS_GFX()
print("Graphics loaded!")

Connecting to WiFi and AWS IoT

Prior to establishing a connection with the AWS MQTT broker, we'll use the esp object to set the AWS device certificate and private key.

Download: file
# Set AWS Device Certificate
esp.set_certificate(DEVICE_CERT)

# Set AWS RSA Private Key
esp.set_private_key(DEVICE_KEY)

Once the certificate and private key have been set, we can connect to the WiFi network and the AWS IoT MQTT broker.

Download: file
# Connect to WiFi
print("Connecting to WiFi...")
wifi.connect()
print("Connected!")

Configure the STEMMA Sensor

An I2C busio device is set up and linked to the soil sensor's address (0x36).

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

MQTT Connection Callback Methods

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

Download: file
# 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 AWS IoT!')
    print('Flags: {0}\nRC: {1}'.format(flags, rc))

    # Subscribe client to all shadow updates
    print("Subscribing to shadow updates...")
    aws_iot.shadow_subscribe()


def disconnect(client, userdata, rc):
    # This method is called when the client disconnects
    # from the broker.
    print('Disconnected from AWS 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.
    print("Message from {}: {}".format(topic, msg))

Connecting to AWS IoT

The code first initializes the AWS MQTT client with the endpoint identifier (broker) and device identifier (client_id).

Download: file
# Set up a new MiniMQTT Client
client =  MQTT(socket,
               broker = secrets['broker'],
               client_id = secrets['client_id'],
               network_manager = wifi)

# Initialize AWS IoT MQTT API Client
aws_iot = MQTT_CLIENT(client)

The connection callback methods created earlier are connected to the aws_iot client and the code attempts to connect to AWS IoT.

Download: file
# Connect callback handlers to AWS IoT MQTT Client
aws_iot.on_connect = connect
aws_iot.on_disconnect = disconnect
aws_iot.on_subscribe = subscribe
aws_iot.on_unsubscribe = unsubscribe
aws_iot.on_publish = publish
aws_iot.on_message = message

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

Once AWS IoT's MQTT broker successfully connects with your client, it'll call the connect() callback method. This method subscribes to the device's shadow topic and listens for updates (aws_iot.shadow_subscribe()). Any data sent to this topic will be received by the code's message() callback.  

Download: file
def connect(client, userdata, flags, rc):
    # This function will be called when the client is connected
    # successfully to the broker.
    print('Connected to AWS IoT!')
    print('Flags: {0}\nRC: {1}'.format(flags, rc))

    # Subscribe client to all shadow updates
    print("Subscribing to shadow updates...")
    aws_iot.shadow_subscribe()

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. 

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

while True:
    try:
        gfx.show_aws_status('Listening for msgs...')
        now = time.monotonic()
        if now - initial > (0.1 * 60):
            # read moisture level
            moisture = ss.moisture_read()
            print("Moisture Level: ", moisture)
            # read temperature
            temperature = ss.get_temp()
            print("Temperature:{}F".format(temperature))
            # Display Soil Sensor values on pyportal
            temperature = gfx.show_temp(temperature)
            gfx.show_water_level(moisture)

We create a JSON-formatted payload (AWS device shadows require this format) to hold both the moisture and temperature. Then, we update the shadow using the handy shadow_update() helper method from the CircuitPython AWS IoT library. 

We'll update the display to show data has been published to AWS IoT and set the timer to the current time.monotonic value.

Download: file
print('Sending data to AWS IoT...')
            gfx.show_aws_status('Publishing data...')
            # Create a json-formatted device payload
            payload = {"state":{"reported":
                        {"moisture":str(moisture),
                        "temp":str(temperature)}}}
            # Update device shadow
            aws_iot.shadow_update(json.dumps(payload))
            gfx.show_aws_status('Data published!')
            print('Data sent!')
            # Reset timer
            initial = now

If the SENSOR_DELAY time has not yet elapsed, we'll poll the AWS MQTT broker to ensure we retain communication with the broker. aws_iot.loop() pings AWS IOT's MQTT broker and listenings for a response back from it. It also queries the broker for any messages received.

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.

This guide was first published on Oct 16, 2019. It was last updated on Oct 16, 2019. This page (Code Walkthrough) was last updated on Feb 17, 2020.