/ pyportal_pet_planter / code.py
code.py
1 # SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 import time 6 7 import board 8 import busio 9 from digitalio import DigitalInOut 10 import adafruit_esp32spi.adafruit_esp32spi_socket as socket 11 from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager 12 import adafruit_imageload 13 import displayio 14 import neopixel 15 from adafruit_bitmap_font import bitmap_font 16 from adafruit_display_text.label import Label 17 from adafruit_io.adafruit_io import IO_MQTT 18 import adafruit_minimqtt.adafruit_minimqtt as MQTT 19 from adafruit_pyportal import PyPortal 20 from adafruit_seesaw.seesaw import Seesaw 21 from simpleio import map_range 22 23 #---| User Config |--------------- 24 25 # How often to poll the soil sensor, in seconds 26 # Polling every 30 seconds or more may cause connection timeouts 27 DELAY_SENSOR = 15 28 29 # How often to send data to adafruit.io, in minutes 30 DELAY_PUBLISH = 5 31 32 # Maximum soil moisture measurement 33 SOIL_LEVEL_MAX = 500.0 34 35 # Minimum soil moisture measurement 36 SOIL_LEVEL_MIN= 350.0 37 38 #---| End User Config |--------------- 39 40 # Background image 41 BACKGROUND = "/images/roots.bmp" 42 # Icons for water level and temperature 43 ICON_LEVEL = "/images/icon-wetness.bmp" 44 ICON_TEMP = "/images/icon-temp.bmp" 45 WATER_COLOR = 0x16549E 46 47 # Audio files 48 wav_water_high = "/sounds/water-high.wav" 49 wav_water_low = "/sounds/water-low.wav" 50 51 # the current working directory (where this file is) 52 cwd = ("/"+__file__).rsplit('/', 1)[0] 53 54 # Get wifi details and more from a secrets.py file 55 try: 56 from secrets import secrets 57 except ImportError: 58 print("WiFi secrets are kept in secrets.py, please add them there!") 59 raise 60 61 # Set up i2c bus 62 i2c_bus = busio.I2C(board.SCL, board.SDA) 63 64 # Initialize soil sensor (s.s) 65 ss = Seesaw(i2c_bus, addr=0x36) 66 67 # PyPortal ESP32 AirLift Pins 68 esp32_cs = DigitalInOut(board.ESP_CS) 69 esp32_ready = DigitalInOut(board.ESP_BUSY) 70 esp32_reset = DigitalInOut(board.ESP_RESET) 71 72 spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 73 esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 74 status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) 75 wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) 76 77 # Initialize PyPortal Display 78 display = board.DISPLAY 79 80 WIDTH = board.DISPLAY.width 81 HEIGHT = board.DISPLAY.height 82 83 # Initialize new PyPortal object 84 pyportal = PyPortal(esp=esp, 85 external_spi=spi) 86 87 # Set backlight level 88 pyportal.set_backlight(0.5) 89 90 # Create a new DisplayIO group 91 splash = displayio.Group() 92 93 # show splash group 94 display.show(splash) 95 96 # Palette for water bitmap 97 palette = displayio.Palette(2) 98 palette[0] = 0x000000 99 palette[1] = WATER_COLOR 100 palette.make_transparent(0) 101 102 # Create water bitmap 103 water_bmp = displayio.Bitmap(display.width, display.height, len(palette)) 104 water = displayio.TileGrid(water_bmp, pixel_shader=palette) 105 splash.append(water) 106 107 print("drawing background..") 108 # Load background image 109 try: 110 bg_bitmap, bg_palette = adafruit_imageload.load(BACKGROUND, 111 bitmap=displayio.Bitmap, 112 palette=displayio.Palette) 113 # Or just use solid color 114 except (OSError, TypeError): 115 BACKGROUND = BACKGROUND if isinstance(BACKGROUND, int) else 0x000000 116 bg_bitmap = displayio.Bitmap(display.width, display.height, 1) 117 bg_palette = displayio.Palette(1) 118 bg_palette[0] = BACKGROUND 119 bg_palette.make_transparent(0) 120 background = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette) 121 122 # Add background to display 123 splash.append(background) 124 125 print('loading fonts...') 126 # Fonts within /fonts/ folder 127 font = cwd+"/fonts/GothamBlack-50.bdf" 128 font_small = cwd+"/fonts/GothamBlack-25.bdf" 129 130 # pylint: disable=syntax-error 131 data_glyphs = b'0123456789FC-* ' 132 font = bitmap_font.load_font(font) 133 font.load_glyphs(data_glyphs) 134 135 font_small = bitmap_font.load_font(font_small) 136 full_glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.: ' 137 font_small.load_glyphs(full_glyphs) 138 139 # Label to display Adafruit IO status 140 label_status = Label(font_small) 141 label_status.x = 305 142 label_status.y = 10 143 splash.append(label_status) 144 145 # Create a label to display the temperature 146 label_temp = Label(font) 147 label_temp.x = 35 148 label_temp.y = 300 149 splash.append(label_temp) 150 151 # Create a label to display the water level 152 label_level = Label(font) 153 label_level.x = display.width - 130 154 label_level.y = 300 155 splash.append(label_level) 156 157 print('loading icons...') 158 # Load temperature icon 159 icon_tmp_bitmap, icon_palette = adafruit_imageload.load(ICON_TEMP, 160 bitmap=displayio.Bitmap, 161 palette=displayio.Palette) 162 icon_palette.make_transparent(0) 163 icon_tmp_bitmap = displayio.TileGrid(icon_tmp_bitmap, 164 pixel_shader=icon_palette, 165 x=0, y=280) 166 splash.append(icon_tmp_bitmap) 167 168 # Load level icon 169 icon_lvl_bitmap, icon_palette = adafruit_imageload.load(ICON_LEVEL, 170 bitmap=displayio.Bitmap, 171 palette=displayio.Palette) 172 icon_palette.make_transparent(0) 173 icon_lvl_bitmap = displayio.TileGrid(icon_lvl_bitmap, 174 pixel_shader=icon_palette, 175 x=315, y=280) 176 splash.append(icon_lvl_bitmap) 177 178 # Connect to WiFi 179 label_status.text = "Connecting..." 180 while not esp.is_connected: 181 try: 182 wifi.connect() 183 except (RuntimeError, ConnectionError) as e: 184 print("could not connect to AP, retrying: ",e) 185 wifi.reset() 186 continue 187 print("Connected to WiFi!") 188 189 # Initialize MQTT interface with the esp interface 190 MQTT.set_socket(socket, esp) 191 192 # Initialize a new MQTT Client object 193 mqtt_client = MQTT.MQTT(broker="io.adafruit.com", 194 username=secrets["aio_user"], 195 password=secrets["aio_key"]) 196 197 # Adafruit IO Callback Methods 198 # pylint: disable=unused-argument 199 def connected(client): 200 # Connected function will be called when the client is connected to Adafruit IO. 201 print('Connected to Adafruit IO!') 202 203 def subscribe(client, userdata, topic, granted_qos): 204 # This method is called when the client subscribes to a new feed. 205 print('Subscribed to {0} with QOS level {1}'.format(topic, granted_qos)) 206 207 # pylint: disable=unused-argument 208 def disconnected(client): 209 # Disconnected function will be called if the client disconnects 210 # from the Adafruit IO MQTT broker. 211 print("Disconnected from Adafruit IO!") 212 213 # Initialize an Adafruit IO MQTT Client 214 io = IO_MQTT(mqtt_client) 215 216 # Connect the callback methods defined above to the Adafruit IO MQTT Client 217 io.on_connect = connected 218 io.on_subscribe = subscribe 219 io.on_disconnect = disconnected 220 221 # Connect to Adafruit IO 222 print("Connecting to Adafruit IO...") 223 io.connect() 224 label_status.text = " " 225 print("Connected!") 226 227 fill_val = 0.0 228 def fill_water(fill_percent): 229 """Fills the background water. 230 :param float fill_percent: Percentage of the display to fill. 231 232 """ 233 assert fill_percent <= 1.0, "Water fill value may not be > 100%" 234 # pylint: disable=global-statement 235 global fill_val 236 237 if fill_val > fill_percent: 238 for _y in range(int((board.DISPLAY.height-1) - ((board.DISPLAY.height-1)*fill_val)), 239 int((board.DISPLAY.height-1) - ((board.DISPLAY.height-1)*fill_percent))): 240 for _x in range(1, board.DISPLAY.width-1): 241 water_bmp[_x, _y] = 0 242 else: 243 for _y in range(board.DISPLAY.height-1, 244 (board.DISPLAY.height-1) - ((board.DISPLAY.height-1)*fill_percent), -1): 245 for _x in range(1, board.DISPLAY.width-1): 246 water_bmp[_x, _y] = 1 247 fill_val = fill_percent 248 249 def display_temperature(temp_val, is_celsius=False): 250 """Displays the temperature from the STEMMA soil sensor 251 on the PyPortal Titano. 252 :param float temp: Temperature value. 253 :param bool is_celsius: 254 255 """ 256 if not is_celsius: 257 temp_val = (temp_val * 9 / 5) + 32 - 15 258 print('Temperature: %0.0fF'%temp_val) 259 label_temp.text = '%0.0fF'%temp_val 260 return int(temp_val) 261 else: 262 print('Temperature: %0.0fC'%temp_val) 263 label_temp.text = '%0.0fC'%temp_val 264 return int(temp_val) 265 266 # initial reference time 267 initial = time.monotonic() 268 while True: 269 # Explicitly pump the message loop 270 # to keep the connection active 271 try: 272 io.loop() 273 except (ValueError, RuntimeError, ConnectionError, OSError) as e: 274 print("Failed to get data, retrying...\n", e) 275 wifi.reset() 276 continue 277 now = time.monotonic() 278 279 print("reading soil sensor...") 280 # Read capactive 281 moisture = ss.moisture_read() 282 label_level.text = str(moisture) 283 284 # Convert into percentage for filling the screen 285 moisture_percentage = map_range(float(moisture), SOIL_LEVEL_MIN, SOIL_LEVEL_MAX, 0.0, 1.0) 286 287 # Read temperature 288 temp = ss.get_temp() 289 temp = display_temperature(temp) 290 291 # fill display 292 print("filling disp..") 293 fill_water(moisture_percentage) 294 print("disp filled..") 295 296 print("temp: " + str(temp) + " moisture: " + str(moisture)) 297 298 # Play water level alarms 299 if moisture <= SOIL_LEVEL_MIN: 300 print("Playing low water level warning...") 301 pyportal.play_file(wav_water_low) 302 elif moisture >= SOIL_LEVEL_MAX: 303 print("Playing high water level warning...") 304 pyportal.play_file(wav_water_high) 305 306 307 if now - initial > (DELAY_PUBLISH * 60): 308 try: 309 print("Publishing data to Adafruit IO...") 310 label_status.text = "Sending to IO..." 311 io.publish("moisture", moisture) 312 io.publish("temperature", temp) 313 print("Published") 314 label_status.text = "Data Sent!" 315 316 # reset timer 317 initial = now 318 except (ValueError, RuntimeError, ConnectionError, OSError) as e: 319 label_status.text = "ERROR!" 320 print("Failed to get data, retrying...\n", e) 321 wifi.reset() 322 time.sleep(DELAY_SENSOR)