Skip to content

Commit 3e010f1

Browse files
authored
Merge pull request #818 from brentru/PyPortal_GCP_IOT_Planter
PyPortal GCP IoT Planter
2 parents d32bb11 + d76713e commit 3e010f1

3 files changed

Lines changed: 340 additions & 0 deletions

File tree

PyPortal_GCP_IOT_Planter/code.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""
2+
PyPortal Google Cloud IoT Core Planter
3+
=======================================================
4+
Water your plant remotely and log its vitals to Google
5+
Cloud IoT Core with your PyPortal.
6+
7+
Author: Brent Rubell for Adafruit Industries, 2019
8+
"""
9+
import time
10+
import json
11+
import board
12+
import busio
13+
import gcp_gfx_helper
14+
import neopixel
15+
from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager
16+
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
17+
from adafruit_gc_iot_core import MQTT_API, Cloud_Core
18+
from adafruit_minimqtt import MQTT
19+
from adafruit_seesaw.seesaw import Seesaw
20+
import digitalio
21+
22+
# Delay before reading the sensors, in minutes
23+
SENSOR_DELAY = 10
24+
25+
# Get wifi details and more from a secrets.py file
26+
try:
27+
from secrets import secrets
28+
except ImportError:
29+
print("WiFi secrets are kept in secrets.py, please add them there!")
30+
raise
31+
32+
# PyPortal ESP32 Setup
33+
esp32_cs = digitalio.DigitalInOut(board.ESP_CS)
34+
esp32_ready = digitalio.DigitalInOut(board.ESP_BUSY)
35+
esp32_reset = digitalio.DigitalInOut(board.ESP_RESET)
36+
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
37+
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
38+
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
39+
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(
40+
esp, secrets, status_light)
41+
42+
# Connect to WiFi
43+
print("Connecting to WiFi...")
44+
wifi.connect()
45+
print("Connected!")
46+
47+
# Soil Sensor Setup
48+
i2c_bus = busio.I2C(board.SCL, board.SDA)
49+
ss = Seesaw(i2c_bus, addr=0x36)
50+
51+
# Water Pump Setup
52+
water_pump = digitalio.DigitalInOut(board.D3)
53+
water_pump.direction = digitalio.Direction.OUTPUT
54+
55+
# Initialize the graphics helper
56+
print("Loading GCP Graphics...")
57+
gfx = gcp_gfx_helper.Google_GFX()
58+
print("Graphics loaded!")
59+
60+
61+
# Define callback methods which are called when events occur
62+
# pylint: disable=unused-argument, redefined-outer-name
63+
def connect(client, userdata, flags, rc):
64+
# This function will be called when the client is connected
65+
# successfully to the broker.
66+
print('Connected to Google Cloud IoT!')
67+
print('Flags: {0}\nRC: {1}'.format(flags, rc))
68+
# Subscribes to commands/# topic
69+
google_mqtt.subscribe_to_all_commands()
70+
71+
72+
def disconnect(client, userdata, rc):
73+
# This method is called when the client disconnects
74+
# from the broker.
75+
print('Disconnected from Google Cloud IoT!')
76+
77+
78+
def subscribe(client, userdata, topic, granted_qos):
79+
# This method is called when the client subscribes to a new topic.
80+
print('Subscribed to {0} with QOS level {1}'.format(topic, granted_qos))
81+
82+
83+
def unsubscribe(client, userdata, topic, pid):
84+
# This method is called when the client unsubscribes from a topic.
85+
print('Unsubscribed from {0} with PID {1}'.format(topic, pid))
86+
87+
88+
def publish(client, userdata, topic, pid):
89+
# This method is called when the client publishes data to a topic.
90+
print('Published to {0} with PID {1}'.format(topic, pid))
91+
92+
93+
def message(client, topic, msg):
94+
# This method is called when the client receives data from a topic.
95+
try:
96+
# Attempt to decode a JSON command
97+
msg_dict = json.loads(msg)
98+
# Handle water-pump commands
99+
if msg_dict['pump_time']:
100+
handle_pump(msg_dict)
101+
except TypeError:
102+
# Non-JSON command, print normally
103+
print("Message from {}: {}".format(topic, msg))
104+
105+
106+
def handle_pump(command):
107+
"""Handles command about the planter's
108+
watering pump from Google Core IoT.
109+
:param json command: Message from device/commands#
110+
"""
111+
print("handling pump...")
112+
# Parse the pump command message
113+
# Expected format: {"power": true, "pump_time":3}
114+
pump_time = command['pump_time']
115+
pump_status = command['power']
116+
if pump_status:
117+
print("Turning pump on for {} seconds...".format(pump_time))
118+
start_pump = time.monotonic()
119+
while True:
120+
gfx.show_gcp_status('Watering plant...')
121+
cur_time = time.monotonic()
122+
if cur_time - start_pump > pump_time:
123+
# Timer expired, leave the loop
124+
print("Plant watered!")
125+
break
126+
water_pump.value = True
127+
gfx.show_gcp_status('Plant watered!')
128+
print("Turning pump off")
129+
water_pump.value = False
130+
131+
132+
# Initialize Google Cloud IoT Core interface
133+
google_iot = Cloud_Core(esp, secrets)
134+
135+
# JSON-Web-Token (JWT) Generation
136+
print("Generating JWT...")
137+
jwt = google_iot.generate_jwt()
138+
print("Your JWT is: ", jwt)
139+
140+
# Set up a new MiniMQTT Client
141+
client = MQTT(socket,
142+
broker=google_iot.broker,
143+
username=google_iot.username,
144+
password=jwt,
145+
client_id=google_iot.cid,
146+
network_manager=wifi)
147+
148+
# Initialize Google MQTT API Client
149+
google_mqtt = MQTT_API(client)
150+
151+
# Connect callback handlers to Google MQTT Client
152+
google_mqtt.on_connect = connect
153+
google_mqtt.on_disconnect = disconnect
154+
google_mqtt.on_subscribe = subscribe
155+
google_mqtt.on_unsubscribe = unsubscribe
156+
google_mqtt.on_publish = publish
157+
google_mqtt.on_message = message
158+
159+
print('Attempting to connect to %s' % client.broker)
160+
google_mqtt.connect()
161+
162+
# Time in seconds since power on
163+
initial = time.monotonic()
164+
165+
while True:
166+
try:
167+
gfx.show_gcp_status('Listening for new messages...')
168+
now = time.monotonic()
169+
if now - initial > (SENSOR_DELAY * 60):
170+
# read moisture level
171+
moisture_level = ss.moisture_read()
172+
# read temperature
173+
temperature = ss.get_temp()
174+
# Display Soil Sensor values on pyportal
175+
temperature = gfx.show_temp(temperature)
176+
gfx.show_water_level(moisture_level)
177+
print('Sending data to GCP IoT Core')
178+
gfx.show_gcp_status('Publishing data...')
179+
google_mqtt.publish(temperature, "events")
180+
time.sleep(2)
181+
google_mqtt.publish(moisture_level, "events")
182+
gfx.show_gcp_status('Data published!')
183+
print('Data sent!')
184+
# Reset timer
185+
initial = now
186+
google_mqtt.loop()
187+
except (ValueError, RuntimeError) as e:
188+
print("Failed to get data, retrying", e)
189+
wifi.reset()
190+
continue
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""
2+
GFX Helper for PyPortal GCP IoT Plant Monitor
3+
"""
4+
import board
5+
import displayio
6+
import terminalio
7+
from adafruit_display_text.label import Label
8+
9+
# the current working directory (where this file is)
10+
cwd = ("/"+__file__).rsplit('/', 1)[0]
11+
12+
# GFX Font
13+
font = terminalio.FONT
14+
15+
class Google_GFX(displayio.Group):
16+
def __init__(self, is_celsius=False):
17+
"""Creates an Google_GFX object for displaying plant
18+
and Google Cloud IoT Core status.
19+
:param bool is_celsius: Temperature displayed in Celsius.
20+
"""
21+
# root displayio group
22+
root_group = displayio.Group(max_size=23)
23+
self.display = board.DISPLAY
24+
self.display.show(root_group)
25+
super().__init__(max_size=15)
26+
27+
# temperature display option
28+
self._is_celsius = is_celsius
29+
30+
# create background icon group
31+
self._icon_group = displayio.Group(max_size=3)
32+
self.append(self._icon_group)
33+
self.display.show(self._icon_group)
34+
# create text object group
35+
self._text_group = displayio.Group(max_size=40)
36+
self.append(self._text_group)
37+
38+
print("Displaying splash screen")
39+
self._icon_sprite = None
40+
self._icon_file = None
41+
self._cwd = cwd
42+
self.set_icon(self._cwd+"/images/gcp_splash.bmp")
43+
44+
print('Setting up labels...')
45+
header_group = displayio.Group(scale=3)
46+
header_label = Label(font, text="Google Cloud IoT\n Planter")
47+
header_label.x = (self.display.width // 2) // 22
48+
header_label.y = 15
49+
header_group.append(header_label)
50+
self._text_group.append(header_group)
51+
52+
# Temperature Display
53+
temp_group = displayio.Group(scale=2, max_size=3)
54+
temp_label = Label(font, text="Temperature: ")
55+
temp_label.x = (self.display.width//2) // 11
56+
temp_label.y = 55
57+
temp_group.append(temp_label)
58+
59+
self.temp_data_label = Label(font, text="75 F")
60+
self.temp_data_label.x = (self.display.width//3)
61+
self.temp_data_label.y = 55
62+
temp_group.append(self.temp_data_label)
63+
self._text_group.append(temp_group)
64+
65+
# Water Level
66+
water_group = displayio.Group(scale=2, max_size=2)
67+
self.water_level = Label(font, text="Water Level: ")
68+
self.water_level.x = (self.display.width//2) // 11
69+
self.water_level.y = 75
70+
water_group.append(self.water_level)
71+
72+
self.water_lvl_label = Label(font, text="350")
73+
self.water_lvl_label.x = (self.display.width//3)
74+
self.water_lvl_label.y = 75
75+
temp_group.append(self.water_lvl_label)
76+
self._text_group.append(water_group)
77+
78+
# GCP Status
79+
status_group = displayio.Group()
80+
self.gcp_status_label = Label(font, text="Connecting to GCP IoT Core...")
81+
self.gcp_status_label.x = self.display.width//4
82+
self.gcp_status_label.y = 200
83+
status_group.append(self.gcp_status_label)
84+
self._text_group.append(status_group)
85+
86+
self.display.show(self._text_group)
87+
88+
def show_gcp_status(self, status_text):
89+
"""Displays the system status on the PyPortal
90+
:param str status_text: Description of current GCP IoT status
91+
"""
92+
self.gcp_status_label.text = status_text
93+
94+
def show_water_level(self, water_data):
95+
"""Displays the water level from the Stemma Soil Sensor.
96+
:param int water_data: water value
97+
"""
98+
print('Water Level: ', water_data)
99+
self.water_lvl_label.text = str(water_data)
100+
101+
def show_temp(self, temp_data):
102+
"""Displays the temperature from the Stemma Soil Sensor.
103+
:param float temp_data: Temperature value.
104+
"""
105+
if not self._is_celsius:
106+
temp_data = (temp_data * 9 / 5) + 32 - 15
107+
print('Temperature: %0.0f°F'%temp_data)
108+
if temp_data >= 212:
109+
self.temp_data_label.color = 0xFD2EE
110+
elif temp_data <= 32:
111+
self.temp_data_label.color = 0xFF0000
112+
self.temp_data_label = '%0.0f°F'%temp_data
113+
temp_data = '%0.0f'%temp_data
114+
return int(temp_data)
115+
else:
116+
print('Temperature: %0.0f°C'%temp_data)
117+
if temp_data <= 0:
118+
self.temp_data_label.color = 0xFD2EE
119+
elif temp_data >= 100:
120+
self.temp_data_label.color = 0xFF0000
121+
self.temp_data_label.text = '%0.0f°C'%temp_data
122+
temp_data = '%0.0f'%temp_data
123+
return int(temp_data)
124+
125+
def set_icon(self, filename):
126+
"""Sets the background image to a bitmap file.
127+
128+
:param filename: The filename of the chosen icon
129+
"""
130+
print("Set icon to ", filename)
131+
if self._icon_group:
132+
self._icon_group.pop()
133+
134+
if not filename:
135+
return # we're done, no icon desired
136+
if self._icon_file:
137+
self._icon_file.close()
138+
self._icon_file = open(filename, "rb")
139+
icon = displayio.OnDiskBitmap(self._icon_file)
140+
try:
141+
self._icon_sprite = displayio.TileGrid(icon,
142+
pixel_shader=displayio.ColorConverter())
143+
except TypeError:
144+
self._icon_sprite = displayio.TileGrid(icon,
145+
pixel_shader=displayio.ColorConverter(),
146+
position=(0,0))
147+
148+
self._icon_group.append(self._icon_sprite)
149+
self.display.refresh_soon()
150+
self.display.wait_for_frame()
225 KB
Binary file not shown.

0 commit comments

Comments
 (0)