Importing CircuitPython Libraries

Download: file
import time
import json
import board
import busio
from digitalio import DigitalInOut
from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
import neopixel
from adafruit_ntp import NTP
from adafruit_azureiot import IoTCentralDevice
from adafruit_seesaw.seesaw import Seesaw

# gfx helper
import azure_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.

The code for this project imports a special adafruit_azureiot library. To help simplify communication between your device and Azure IoT Central, we wrote a CircuitPython helper module called Adafruit_CircuitPython_AzureIoT

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

Configuring the Graphical Helper 

The graphics helper, which manages' the PyPortal's display is created. Once created it shows a splash screen whilst the device connects to WiFi and Azure IoT Central. If you want to show the temperature in Fahrenheit instead of Celsius, change is_celsius=True to is_celsius=False.

Download: file
# init. graphics helper
gfx = azure_gfx_helper.Azure_GFX(is_celsius=True)

Configuring the PyPortal's WiFi

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 to manage the WiFi connection.

Finally a Network Time Protocol, or NTP object is set up to synchronize the current time from a server.

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

# Set up the WiFi manager with a status light to show the WiFi connection status
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)
print("WiFi connecting...")
wifi.connect()
print("WiFi connected!")

# Time setup, needed to authenticate with Azure IoT Central
ntp = NTP(esp)
while not ntp.valid_time:
    print("Failed to obtain time, retrying in 5 seconds...")
    time.sleep(5)
    ntp.set_time()

Configuring the Soil 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)

Configuring the Azure IoT Central Module

Next, we'll create an instance of the Azure IoT Central device client called device. It takes in the ESP network socket, socket, the ESP object, esp, and the IoT Central connection details from the secrets file. Once created, the device connects to IoT Central.

Download: file
# Create an instance of the Azure IoT Central device
device = IoTCentralDevice(
    socket,
    esp,
    secrets["id_scope"],
    secrets["device_id"],
    secrets["key"]
)

# Connect to Azure IoT Central
device.connect()

Hide the splash screen and show the telemetry

Once the device is connected, the graphical helper can hide the splash screen and start displaying the telemetry values on screen.

Download: file
# Hide the splash screen and show the telemetry values
gfx.show_text()

Main Loop

The first part of the main loop reads the soil sensor's (ss) moisture level and temperature. It then calls gfx.display_moisture and gfx.display_temp to display the moisture and temperature values on the PyPortal's display

Download: file
while True:
    try:
        # read moisture level
        moisture_level = ss.moisture_read()
        # read temperature
        temperature = ss.get_temp()
        # display soil sensor values on pyportal
        gfx.display_moisture(moisture_level)
        gfx.display_temp(temperature)

The next block of code changes the displays status to indicate that data is about to be sent. It then builds a JSON object, message, containing the temperature and soil moisture.

This message is then sent to Azure IoT Central as telemetry data using the device.send_telemetry() call. The device.loop() method is then called to process any communication back and further between the Azure IoT Central module and the Azure IoT Central application.

Finally the display status is changed to show data has been sent.

Download: file
# send the temperature and moisture level to Azure
message = {
    "Temperature": temperature,
    "MoistureLevel": moisture_level
}
device.send_telemetry(json.dumps(message))
device.loop()

gfx.display_azure_status('Data sent!')
print('Data sent!')

All of this code is wrapped in a try/except control flow block. If connection is lost at any point, then the except code will be run before going back to the try.

This except code block will reset and reconnect the wifi object, then reconnect to Azure IoT Central.

Download: file
except (ValueError, RuntimeError) as e:
    print("Failed to get data, retrying\n", e)
    wifi.reset()
    wifi.connect()
    device.reconnect()
    continue

Finally the loop sleeps for 10 minutes, represented as 600 seconds. This is to ensure IoT Central doesn't receive too much data - the S1 tier is limited to 5,000 messages a month, and one message every 10 minutes is a maximum of 4,464 messages a month.

Download: file
# Sleep for 10 minutes before getting the next value
time.sleep(600)
This guide was first published on May 24, 2019. It was last updated on May 24, 2019.
This page (Code Walkthrough) was last updated on Oct 18, 2020.