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