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)