/ literary-clock / code.py
code.py
  1  # SPDX-FileCopyrightText: 2022 Eva Herrada for Adafruit Industries
  2  # SPDX-License-Identifier: MIT
  3  
  4  import time
  5  
  6  import ssl
  7  import gc
  8  import socketpool
  9  import wifi
 10  import adafruit_minimqtt.adafruit_minimqtt as MQTT
 11  from adafruit_io.adafruit_io import IO_MQTT
 12  import adafruit_datetime
 13  import adafruit_display_text
 14  from adafruit_display_text import label
 15  import board
 16  from adafruit_bitmap_font import bitmap_font
 17  import displayio
 18  from adafruit_display_shapes.rect import Rect
 19  
 20  UTC_OFFSET = -4
 21  
 22  quotes = {}
 23  with open("quotes.csv", "r", encoding="UTF-8") as F:
 24      for quote_line in F:
 25          split = quote_line.split("|")
 26          quotes[split[0]] = split[1:]
 27  
 28  display = board.DISPLAY
 29  splash = displayio.Group()
 30  display.show(splash)
 31  
 32  arial = bitmap_font.load_font("fonts/Arial-12.pcf")
 33  bold = bitmap_font.load_font("fonts/Arial-Bold-12.pcf")
 34  LINE_SPACING = 0.8
 35  HEIGHT = arial.get_bounding_box()[1]
 36  QUOTE_X = 10
 37  QUOTE_Y = 7
 38  
 39  rect = Rect(0, 0, 296, 128, fill=0xFFFFFF, outline=0xFFFFFF)
 40  splash.append(rect)
 41  
 42  quote = label.Label(
 43      font=arial,
 44      x=QUOTE_X,
 45      y=QUOTE_Y,
 46      color=0x000000,
 47      line_spacing=LINE_SPACING,
 48  )
 49  
 50  splash.append(quote)
 51  time_label = label.Label(
 52      font=bold,
 53      color=0x000000,
 54      line_spacing=LINE_SPACING,
 55  )
 56  splash.append(time_label)
 57  
 58  time_label_2 = label.Label(
 59      font=bold,
 60      color=0x000000,
 61      line_spacing=LINE_SPACING,
 62  )
 63  splash.append(time_label_2)
 64  
 65  after_label = label.Label(
 66      font=arial,
 67      color=0x000000,
 68      line_spacing=LINE_SPACING,
 69  )
 70  splash.append(after_label)
 71  
 72  after_label_2 = label.Label(
 73      font=arial,
 74      color=0x000000,
 75      line_spacing=LINE_SPACING,
 76  )
 77  splash.append(after_label_2)
 78  
 79  author_label = label.Label(
 80      font=arial, x=QUOTE_X, y=115, color=0x000000, line_spacing=LINE_SPACING
 81  )
 82  splash.append(author_label)
 83  
 84  try:
 85      from secrets import secrets
 86  except ImportError:
 87      print("WiFi secrets are kept in secrets.py, please add them there!")
 88      raise
 89  
 90  aio_username = secrets["aio_username"]
 91  aio_key = secrets["aio_key"]
 92  
 93  print(f"Connecting to {secrets['ssid']}")
 94  wifi.radio.connect(secrets["ssid"], secrets["password"])
 95  print(f"Connected to {secrets['ssid']}!")
 96  
 97  
 98  def get_width(font, text):
 99      return sum(font.get_glyph(ord(c)).shift_x for c in text)
100  
101  
102  def smart_split(text, font, width):
103      words = ""
104      spl = text.split(" ")
105      for i, word in enumerate(spl):
106          words += f" {word}"
107          lwidth = get_width(font, words)
108          if width + lwidth > 276:
109              spl[i] = "\n" + spl[i]
110              text = " ".join(spl)
111              break
112      return text
113  
114  
115  def connected(client):  # pylint: disable=unused-argument
116      io.subscribe_to_time("iso")
117  
118  
119  def disconnected(client):  # pylint: disable=unused-argument
120      print("Disconnected from Adafruit IO!")
121  
122  
123  def update_text(hour_min):
124      quote.text = (
125          time_label.text
126      ) = time_label_2.text = after_label.text = after_label_2.text = ""
127  
128      before, time_text, after = quotes[hour_min][0].split("^")
129      text = adafruit_display_text.wrap_text_to_pixels(before, 276, font=arial)
130      quote.text = "\n".join(text)
131  
132      for line in text:
133          width = get_width(arial, line)
134  
135      time_text = smart_split(time_text, bold, width)
136  
137      split_time = time_text.split("\n")
138      if time_text[0] != "\n":
139          time_label.x = time_x = QUOTE_X + width
140          time_label.y = time_y = QUOTE_Y + int((len(text) - 1) * HEIGHT * LINE_SPACING)
141          time_label.text = split_time[0]
142      if "\n" in time_text:
143          time_label_2.x = time_x = QUOTE_X
144          time_label_2.y = time_y = QUOTE_Y + int(len(text) * HEIGHT * LINE_SPACING)
145          wrapped = adafruit_display_text.wrap_text_to_pixels(
146              split_time[1], 276, font=arial
147          )
148          time_label_2.text = "\n".join(wrapped)
149      width = get_width(bold, split_time[-1]) + time_x - QUOTE_X
150  
151      if after:
152          after = smart_split(after, arial, width)
153  
154          split_after = after.split("\n")
155          if after[0] != "\n":
156              after_label.x = QUOTE_X + width
157              after_label.y = time_y
158              after_label.text = split_after[0]
159          if "\n" in after:
160              after_label_2.x = QUOTE_X
161              after_label_2.y = time_y + int(HEIGHT * LINE_SPACING)
162              wrapped = adafruit_display_text.wrap_text_to_pixels(
163                  split_after[1], 276, font=arial
164              )
165              after_label_2.text = "\n".join(wrapped)
166  
167      author = f"{quotes[hour_min][2]} - {quotes[hour_min][1]}"
168      author_label.text = adafruit_display_text.wrap_text_to_pixels(
169          author, 276, font=arial
170      )[0]
171      time.sleep(display.time_to_refresh + 0.1)
172      display.refresh()
173  
174  
175  LAST = None
176  
177  
178  def message(client, feed_id, payload):  # pylint: disable=unused-argument
179      global LAST  # pylint: disable=global-statement
180      timezone = adafruit_datetime.timezone.utc
181      timezone._offset = adafruit_datetime.timedelta(  # pylint: disable=protected-access
182          seconds=UTC_OFFSET * 3600
183      )
184      datetime = adafruit_datetime.datetime.fromisoformat(payload[:-1]).replace(
185          tzinfo=timezone
186      )
187      local_datetime = datetime.tzinfo.fromutc(datetime)
188      print(local_datetime)
189      hour_min = f"{local_datetime.hour:02}:{local_datetime.minute:02}"
190      if local_datetime.minute != LAST:
191          if hour_min in quotes:
192              update_text(hour_min)
193  
194      LAST = local_datetime.minute
195      gc.collect()
196  
197  
198  # Create a socket pool
199  pool = socketpool.SocketPool(wifi.radio)
200  
201  # Initialize a new MQTT Client object
202  mqtt_client = MQTT.MQTT(
203      broker="io.adafruit.com",
204      port=1883,
205      username=secrets["aio_username"],
206      password=secrets["aio_key"],
207      socket_pool=pool,
208      ssl_context=ssl.create_default_context(),
209  )
210  
211  # Initialize an Adafruit IO MQTT Client
212  io = IO_MQTT(mqtt_client)
213  
214  # Connect the callback methods defined above to Adafruit IO
215  io.on_connect = connected
216  io.on_disconnect = disconnected
217  io.on_message = message
218  
219  # Connect to Adafruit IO
220  print("Connecting to Adafruit IO...")
221  io.connect()
222  
223  while True:
224      try:
225          io.loop()
226      except (ValueError, RuntimeError) as e:
227          print("Failed to get data, retrying\n", e)
228          wifi.reset()
229          io.reconnect()
230          continue
231      time.sleep(1)