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
- For more information about using the MQTT protocol with CircuitPython - check out our MQTT in CircuitPython guide on this topic here.
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.
- For a complete explanation of how MiniMQTT's callback methods work, click here.
# 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
Page last edited April 09, 2024
Text editor powered by tinymce.