Skip to content

Commit 115c6ee

Browse files
authored
Merge pull request #3178 from FoamyGuy/magtag_literary_clock
adding MagTag literature clock
2 parents a71973f + 076ce9c commit 115c6ee

File tree

1 file changed

+261
-0
lines changed
  • MagTag/MagTag_Literature_Clock

1 file changed

+261
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# SPDX-FileCopyrightText: 2025 Tim C, written for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
"""
5+
Literature Quotes Clock for the Adafruit MagTag
6+
7+
This project displays the current time by showing a quote from a book
8+
that references the time. Every minute of the day has a different quote.
9+
The current time reference portion of the quote is accented with an outline
10+
to make it easier to see at a glance.
11+
12+
This project was inspired by:
13+
- https://github.com/JohannesNE/literature-clock
14+
- https://www.instructables.com/Literary-Clock-Made-From-E-reader/
15+
16+
The quotes were sourced originally by The Guardian:
17+
https://www.theguardian.com/books/table/2011/apr/21/literary-clock?CMP=twt_gu
18+
"""
19+
import os
20+
import random
21+
import time
22+
import zlib
23+
24+
import displayio
25+
import rtc
26+
import socketpool
27+
import supervisor
28+
import terminalio
29+
import wifi
30+
31+
import adafruit_ntp
32+
from adafruit_display_text.bitmap_label import Label
33+
from adafruit_display_text import wrap_text_to_pixels
34+
35+
# Set the current offset for your timezone
36+
TZ_OFFSET_FROM_UTC = -6
37+
38+
39+
def which_line_contains(all_lines, important_passage):
40+
"""
41+
Find the line that contains the important passage
42+
:param all_lines: The lines to search
43+
:param important_passage: The passage to search for
44+
:return: The index of the line that contains the important passage
45+
or None if it was not found
46+
"""
47+
48+
index_within_spaced_version = " ".join(all_lines).find(important_passage)
49+
50+
working_index = 0
51+
for i in range(len(all_lines)):
52+
line = all_lines[i]
53+
if working_index <= index_within_spaced_version < working_index + len(line):
54+
return i
55+
working_index += len(line) + 1 # extra 1 for the newline
56+
57+
return None
58+
59+
60+
def find_lines_to_show(all_lines, important_passage):
61+
"""
62+
Find the line that contains ``important_passage`` and return
63+
the start of the range of 7 lines that provides the largest possible
64+
context around it.
65+
66+
:param all_lines: The lines to search
67+
:param important_passage: The passage to search for
68+
:return: index of the first line in a range of 7 lines with the widest context.
69+
"""
70+
if len(all_lines) <= 7:
71+
return 0
72+
73+
try:
74+
passage_line = which_line_contains(all_lines, important_passage)
75+
if passage_line <= 3:
76+
return 0
77+
except TypeError as e:
78+
raise TypeError(f"ip: {important_passage} | {all_lines}") from e
79+
80+
if passage_line >= len(all_lines) - 4:
81+
return len(all_lines) - 7
82+
83+
return passage_line - 3
84+
85+
86+
display = supervisor.runtime.display
87+
88+
# WIFI Setup
89+
wifi_ssid = os.getenv("CIRCUITPY_WIFI_SSID")
90+
wifi_password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
91+
if wifi_ssid is None:
92+
print("WiFi credentials are kept in settings.toml, please add them there!")
93+
raise ValueError("SSID not found in environment variables")
94+
95+
try:
96+
wifi.radio.connect(wifi_ssid, wifi_password)
97+
except ConnectionError:
98+
print("Failed to connect to WiFi with provided credentials")
99+
raise
100+
101+
# Wait a few seconds for WIFI to finish connecting and be ready
102+
time.sleep(2)
103+
104+
# Fetch time from NTP
105+
pool = socketpool.SocketPool(wifi.radio)
106+
ntp = adafruit_ntp.NTP(
107+
pool,
108+
server="0.adafruit.pool.ntp.org",
109+
tz_offset=TZ_OFFSET_FROM_UTC,
110+
cache_seconds=3600,
111+
)
112+
113+
# Update the system RTC from the NTP time
114+
rtc.RTC().datetime = ntp.datetime
115+
116+
# main group to hold all other visual elements
117+
main_group = displayio.Group()
118+
119+
# background group used for white background behind the quote
120+
# scale 8x to save memory on the Bitmap
121+
bg_group = displayio.Group(scale=8)
122+
123+
# Create & append Bitmap for the white background
124+
bg_bmp = displayio.Bitmap(display.width // 8, display.height // 8, 1)
125+
bg_palette = displayio.Palette(1)
126+
bg_palette[0] = 0xFFFFFF
127+
bg_tg = displayio.TileGrid(bg_bmp, pixel_shader=bg_palette)
128+
bg_group.append(bg_tg)
129+
main_group.append(bg_group)
130+
131+
# Setup accent palette for the outlined text
132+
accent_palette = displayio.Palette(5)
133+
accent_palette[3] = 0x666666
134+
accent_palette[4] = 0xFFFFFF
135+
136+
# Setup BitmapLabel to show the quote
137+
quote_lbl = Label(
138+
terminalio.FONT, text="", color=0x666666, color_palette=accent_palette
139+
)
140+
quote_lbl.anchor_point = (0, 0)
141+
quote_lbl.anchored_position = (2, 2)
142+
main_group.append(quote_lbl)
143+
144+
# Setup BitmapLabel to show book title and author
145+
book_info_lbl = Label(terminalio.FONT, text="", color=0x666666)
146+
book_info_lbl.anchor_point = (0, 1.0) # place it at the bottom of the display
147+
book_info_lbl.anchored_position = (2, display.height - 2)
148+
main_group.append(book_info_lbl)
149+
150+
# Set main group containing visual elements to show on the display
151+
display.root_group = main_group
152+
153+
while True:
154+
# get the current time from system RTC
155+
now = time.localtime()
156+
157+
# break out the current hour in 24hr format
158+
hour = f"{now.tm_hour:02d}"
159+
160+
# open the data file for the current hour
161+
with open(f"split_data_compressed/{hour}.csv.gz", "rb") as f:
162+
# read and unzip the data
163+
compressed_data = f.read()
164+
rows = zlib.decompress(compressed_data).split(b"\n")
165+
166+
# break out the current minute
167+
current_minute = f"{now.tm_min:02d}".encode("utf-8")
168+
169+
print(f"hour: {hour} min: {current_minute}")
170+
171+
# list to hold possible quotes for the current time
172+
options = []
173+
174+
# get the previous minute also for alternate choices
175+
previous_minute = f"{now.tm_min - 1:02d}".encode("utf-8")
176+
# list to hold alternate choices
177+
alternates = []
178+
179+
# loop over all rows in the data from the CSV
180+
for row in rows:
181+
# if the current row is for the current time
182+
if row[3:5] == current_minute:
183+
# add the current row as a potential choice to show
184+
options.append(row)
185+
186+
# if the current row is for the previous minute
187+
if row[3:5] == previous_minute:
188+
# add the current row as an alternate choice
189+
alternates.append(row)
190+
191+
# if there is at least one option for the current time
192+
if len(options) > 0:
193+
# make a random choice from the possible quote options
194+
choice = random.choice(options)
195+
196+
else: # No options for current time
197+
# use a random choice from the previous minute instead
198+
choice = random.choice(alternates)
199+
200+
# decode the row of data from bytes to a string
201+
row_str = choice.decode("utf-8")
202+
203+
# split the data on the pipe character
204+
parts = row_str.split("|")
205+
206+
# extract the quote text
207+
quote = parts[2]
208+
209+
# extract the author
210+
author = parts[4]
211+
212+
# extract the book title
213+
title = parts[3]
214+
215+
# set the text in the book info label to show the title and author
216+
book_info_lbl.text = f"{title} - {author}"
217+
218+
# extract the current time reference string
219+
time_part = parts[1]
220+
221+
# get start and end indexes of the time reference
222+
time_start_index = quote.find(time_part)
223+
time_end_index = time_start_index + len(time_part)
224+
225+
# split the quote text into lines with a maximum width that fits
226+
# on the MagTag display
227+
quote_lines = wrap_text_to_pixels(
228+
quote,
229+
display.width - 4,
230+
terminalio.FONT,
231+
outline_accent_ranges=[
232+
(time_start_index, time_end_index, quote_lbl.outline_size)
233+
],
234+
)
235+
236+
# remove previous accents
237+
quote_lbl.clear_accent_ranges()
238+
239+
# find the index of the first line we want to show.
240+
# only relevant for long quotes, short ones will be shown in full
241+
first_line_to_show = find_lines_to_show(quote_lines, time_part)
242+
243+
# Temporary version of final visible quote joined with spaces instead of newlines,
244+
# so we can search for the time_part without worrying about potential newlines.
245+
shown_quote_with_spaces = " ".join(
246+
quote_lines[first_line_to_show : first_line_to_show + 7]
247+
)
248+
249+
# find the current time reference within the quote that will be shown
250+
time_start_index = shown_quote_with_spaces.find(time_part)
251+
time_end_index = time_start_index + len(time_part)
252+
253+
# wrap the quote to be shown to multiple lines and set it on the label
254+
quote_lbl.text = "\n".join(quote_lines[first_line_to_show : first_line_to_show + 7])
255+
256+
# accent the part of the quote that references the current time
257+
quote_lbl.add_accent_range(time_start_index, time_end_index, 4, 3, "outline")
258+
259+
# update the display and wait 60 seconds
260+
display.refresh()
261+
time.sleep(60)

0 commit comments

Comments
 (0)