Skip to content

Commit d2f16f8

Browse files
authored
Merge pull request #3046 from adafruit/led_clock
adding led matrix clock code
2 parents a87f6ab + 0276850 commit d2f16f8

File tree

3 files changed

+385
-0
lines changed

3 files changed

+385
-0
lines changed

LED_Matrix_Clock/code.py

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
'''LED Matrix Alarm Clock'''
5+
import os
6+
import ssl
7+
import time
8+
import random
9+
import wifi
10+
import socketpool
11+
import microcontroller
12+
import board
13+
import audiocore
14+
import audiobusio
15+
import audiomixer
16+
import adafruit_is31fl3741
17+
from adafruit_is31fl3741.adafruit_rgbmatrixqt import Adafruit_RGBMatrixQT
18+
import adafruit_ntp
19+
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
20+
from rainbowio import colorwheel
21+
from adafruit_seesaw import digitalio, rotaryio, seesaw
22+
from adafruit_debouncer import Button
23+
24+
timezone = -4 # your timezone offset
25+
alarm_hour = 14 # hour is 24 hour for alarm to denote am/pm
26+
alarm_min = 11 # minutes
27+
alarm_volume = 1 # float 0.0 to 1.0
28+
hour_12 = True # 12 hour or 24 hour time
29+
BRIGHTNESS = 128 # led brightness (0-255)
30+
31+
# I2S pins for Audio BFF
32+
DATA = board.A0
33+
LRCLK = board.A1
34+
BCLK = board.A2
35+
36+
# connect to WIFI
37+
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
38+
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
39+
40+
context = ssl.create_default_context()
41+
pool = socketpool.SocketPool(wifi.radio)
42+
ntp = adafruit_ntp.NTP(pool, tz_offset=timezone, cache_seconds=3600)
43+
44+
# Initialize I2C
45+
i2c = board.STEMMA_I2C()
46+
47+
# Initialize both matrix displays
48+
matrix1 = Adafruit_RGBMatrixQT(i2c, address=0x30, allocate=adafruit_is31fl3741.PREFER_BUFFER)
49+
matrix2 = Adafruit_RGBMatrixQT(i2c, address=0x31, allocate=adafruit_is31fl3741.PREFER_BUFFER)
50+
matrix1.global_current = 0x05
51+
matrix2.global_current = 0x05
52+
matrix1.set_led_scaling(BRIGHTNESS)
53+
matrix2.set_led_scaling(BRIGHTNESS)
54+
matrix1.enable = True
55+
matrix2.enable = True
56+
matrix1.fill(0x000000)
57+
matrix2.fill(0x000000)
58+
matrix1.show()
59+
matrix2.show()
60+
61+
audio = audiobusio.I2SOut(BCLK, LRCLK, DATA)
62+
wavs = []
63+
for filename in os.listdir('/'):
64+
if filename.lower().endswith('.wav') and not filename.startswith('.'):
65+
wavs.append("/"+filename)
66+
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
67+
bits_per_sample=16, samples_signed=True, buffer_size=32768)
68+
mixer.voice[0].level = alarm_volume
69+
wav_filename = wavs[random.randint(0, (len(wavs))-1)]
70+
wav_file = open(wav_filename, "rb")
71+
audio.play(mixer)
72+
73+
def open_audio():
74+
n = wavs[random.randint(0, (len(wavs))-1)]
75+
f = open(n, "rb")
76+
w = audiocore.WaveFile(f)
77+
return w
78+
79+
seesaw = seesaw.Seesaw(i2c, addr=0x36)
80+
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
81+
ss_pin = digitalio.DigitalIO(seesaw, 24)
82+
button = Button(ss_pin, long_duration_ms=1000)
83+
84+
button_held = False
85+
encoder = rotaryio.IncrementalEncoder(seesaw)
86+
last_position = 0
87+
88+
# Simple 5x7 font bitmap patterns for digits 0-9
89+
FONT_5X7 = {
90+
'0': [
91+
0b01110,
92+
0b10001,
93+
0b10011,
94+
0b10101,
95+
0b11001,
96+
0b10001,
97+
0b01110
98+
],
99+
'1': [
100+
0b00100,
101+
0b01100,
102+
0b00100,
103+
0b00100,
104+
0b00100,
105+
0b00100,
106+
0b01110
107+
],
108+
'2': [
109+
0b01110,
110+
0b10001,
111+
0b00001,
112+
0b00010,
113+
0b00100,
114+
0b01000,
115+
0b11111
116+
],
117+
'3': [
118+
0b11111,
119+
0b00010,
120+
0b00100,
121+
0b00010,
122+
0b00001,
123+
0b10001,
124+
0b01110
125+
],
126+
'4': [
127+
0b00010,
128+
0b00110,
129+
0b01010,
130+
0b10010,
131+
0b11111,
132+
0b00010,
133+
0b00010
134+
],
135+
'5': [
136+
0b11111,
137+
0b10000,
138+
0b11110,
139+
0b00001,
140+
0b00001,
141+
0b10001,
142+
0b01110
143+
],
144+
'6': [
145+
0b00110,
146+
0b01000,
147+
0b10000,
148+
0b11110,
149+
0b10001,
150+
0b10001,
151+
0b01110
152+
],
153+
'7': [
154+
0b11111,
155+
0b00001,
156+
0b00010,
157+
0b00100,
158+
0b01000,
159+
0b01000,
160+
0b01000
161+
],
162+
'8': [
163+
0b01110,
164+
0b10001,
165+
0b10001,
166+
0b01110,
167+
0b10001,
168+
0b10001,
169+
0b01110
170+
],
171+
'9': [
172+
0b01110,
173+
0b10001,
174+
0b10001,
175+
0b01111,
176+
0b00001,
177+
0b00010,
178+
0b01100
179+
],
180+
' ': [
181+
0b00000,
182+
0b00000,
183+
0b00000,
184+
0b00000,
185+
0b00000,
186+
0b00000,
187+
0b00000
188+
]
189+
}
190+
191+
def draw_pixel_flipped(matrix, x, y, color):
192+
"""Draw a pixel with 180-degree rotation"""
193+
flipped_x = 12 - x
194+
flipped_y = 8 - y
195+
if 0 <= flipped_x < 13 and 0 <= flipped_y < 9:
196+
matrix.pixel(flipped_x, flipped_y, color)
197+
198+
def draw_char(matrix, char, x, y, color):
199+
"""Draw a character at position x,y on the specified matrix (flipped)"""
200+
if char in FONT_5X7:
201+
bitmap = FONT_5X7[char]
202+
for row in range(7):
203+
for col in range(5):
204+
if bitmap[row] & (1 << (4 - col)):
205+
draw_pixel_flipped(matrix, x + col, y + row, color)
206+
207+
def draw_colon_split(y, color):
208+
"""Draw a split colon with 2x2 dots between the displays"""
209+
# Top dot - left half on matrix1, right half on matrix2
210+
draw_pixel_flipped(matrix1, 12, y+1, color) # Top-left
211+
draw_pixel_flipped(matrix1, 12, y + 2, color) # Bottom-left
212+
draw_pixel_flipped(matrix2, 0, y+1, color) # Top-right
213+
draw_pixel_flipped(matrix2, 0, y + 2, color) # Bottom-right
214+
215+
# Bottom dot - left half on matrix1, right half on matrix2
216+
draw_pixel_flipped(matrix1, 12, y + 4, color) # Top-left
217+
draw_pixel_flipped(matrix1, 12, y + 5, color) # Bottom-left
218+
draw_pixel_flipped(matrix2, 0, y + 4, color) # Top-right
219+
draw_pixel_flipped(matrix2, 0, y + 5, color) # Bottom-right
220+
221+
def draw_text(text, color=0xFFFFFF):
222+
"""Draw text across both matrices with proper spacing"""
223+
# Clear both displays
224+
matrix1.fill(0x000000)
225+
matrix2.fill(0x000000)
226+
227+
# For "12:00" layout with spacing:
228+
# "1" at x=0 on matrix1 (5 pixels wide)
229+
# "2" at x=6 on matrix1 (5 pixels wide, leaving 1-2 pixels space before colon)
230+
# ":" split between matrix1 and matrix2
231+
# "0" at x=2 on matrix2 (leaving 1-2 pixels space after colon)
232+
# "0" at x=8 on matrix2 (5 pixels wide)
233+
234+
y = 1 # Vertical position
235+
236+
# Draw first two digits on matrix1
237+
if len(text) >= 2:
238+
draw_char(matrix1, text[0], 0, y, color) # First digit at x=0
239+
draw_char(matrix1, text[1], 6, y, color) # Second digit at x=6 (leaves space for colon)
240+
241+
# Draw the colon split between displays
242+
if len(text) >= 3 and text[2] == ':':
243+
draw_colon_split(y, color)
244+
245+
# Draw last two digits on matrix2
246+
if len(text) >= 5:
247+
draw_char(matrix2, text[3], 2, y, color) # Third digit at x=2 (leaves space after colon)
248+
draw_char(matrix2, text[4], 8, y, color) # Fourth digit at x=8
249+
250+
# Update both displays
251+
matrix1.show()
252+
matrix2.show()
253+
print("updated matrices")
254+
255+
refresh_clock = ticks_ms()
256+
refresh_timer = 3600 * 1000
257+
clock_clock = ticks_ms()
258+
clock_timer = 1000
259+
first_run = True
260+
new_time = False
261+
color_value = 0
262+
COLOR = colorwheel(0)
263+
time_str = "00:00"
264+
set_alarm = 0
265+
active_alarm = False
266+
alarm = f"{alarm_hour:02}:{alarm_min:02}"
267+
268+
while True:
269+
270+
button.update()
271+
if button.long_press:
272+
# long press to set alarm & turn off alarm
273+
if set_alarm == 0 and not active_alarm:
274+
set_alarm = 1
275+
draw_text(f"{alarm_hour:02}: ", COLOR)
276+
if active_alarm:
277+
mixer.voice[0].stop()
278+
active_alarm = False
279+
BRIGHTNESS = 128
280+
matrix1.set_led_scaling(BRIGHTNESS)
281+
matrix2.set_led_scaling(BRIGHTNESS)
282+
if button.short_count:
283+
# short press to set hour and minute
284+
set_alarm = (set_alarm + 1) % 3
285+
if set_alarm == 0:
286+
draw_text(time_str, COLOR)
287+
elif set_alarm == 2:
288+
draw_text(f" :{alarm_min:02}", COLOR)
289+
290+
position = -encoder.position
291+
if position != last_position:
292+
if position > last_position:
293+
# when setting alarm, rotate through hours/minutes
294+
# when not, change color for LEDs
295+
if set_alarm == 0:
296+
color_value = (color_value + 5) % 255
297+
elif set_alarm == 1:
298+
alarm_hour = (alarm_hour + 1) % 24
299+
elif set_alarm == 2:
300+
alarm_min = (alarm_min + 1) % 60
301+
else:
302+
if set_alarm == 0:
303+
color_value = (color_value - 5) % 255
304+
elif set_alarm == 1:
305+
alarm_hour = (alarm_hour - 1) % 24
306+
elif set_alarm == 2:
307+
alarm_min = (alarm_min - 1) % 60
308+
alarm = f"{alarm_hour:02}:{alarm_min:02}"
309+
COLOR = colorwheel(color_value)
310+
if set_alarm == 0:
311+
draw_text(time_str, COLOR)
312+
elif set_alarm == 1:
313+
draw_text(f"{alarm_hour:02}: ", COLOR)
314+
elif set_alarm == 2:
315+
draw_text(f" :{alarm_min:02}", COLOR)
316+
last_position = position
317+
318+
# resync with NTP time server every hour
319+
if set_alarm == 0:
320+
if ticks_diff(ticks_ms(), refresh_clock) >= refresh_timer or first_run:
321+
try:
322+
print("Getting time from internet!")
323+
now = ntp.datetime
324+
print(now)
325+
total_seconds = time.mktime(now)
326+
first_run = False
327+
am_pm_hour = now.tm_hour
328+
if hour_12:
329+
hours = am_pm_hour % 12
330+
if hours == 0:
331+
hours = 12
332+
else:
333+
hours = am_pm_hour
334+
time_str = f"{hours:02}:{now.tm_min:02}"
335+
print(time_str)
336+
mins = now.tm_min
337+
seconds = now.tm_sec
338+
draw_text(time_str, COLOR)
339+
refresh_clock = ticks_add(refresh_clock, refresh_timer)
340+
except Exception as e: # pylint: disable=broad-except
341+
print("Some error occured, retrying! -", e)
342+
time.sleep(10)
343+
microcontroller.reset()
344+
345+
# keep time locally between NTP server syncs
346+
if ticks_diff(ticks_ms(), clock_clock) >= clock_timer:
347+
seconds += 1
348+
# print(seconds)
349+
if seconds > 59:
350+
mins += 1
351+
seconds = 0
352+
new_time = True
353+
if mins > 59:
354+
am_pm_hour += 1
355+
mins = 0
356+
new_time = True
357+
if hour_12:
358+
hours = am_pm_hour % 12
359+
if hours == 0:
360+
hours = 12
361+
else:
362+
hours = am_pm_hour
363+
if new_time:
364+
time_str = f"{hours:02}:{mins:02}"
365+
new_time = False
366+
print(time_str)
367+
draw_text(time_str, COLOR)
368+
if f"{am_pm_hour:02}:{mins:02}" == alarm:
369+
print("alarm!")
370+
# grab a new wav file from the wavs list
371+
wave = open_audio()
372+
active_alarm = True
373+
if active_alarm:
374+
# blink the clock characters
375+
if BRIGHTNESS:
376+
BRIGHTNESS = 0
377+
else:
378+
BRIGHTNESS = 128
379+
matrix1.set_led_scaling(BRIGHTNESS)
380+
matrix2.set_led_scaling(BRIGHTNESS)
381+
clock_clock = ticks_add(clock_clock, clock_timer)
382+
383+
# loop alarm wav
384+
if active_alarm:
385+
mixer.voice[0].play(wave, loop=True)

LED_Matrix_Clock/nice-alarm.wav

1.14 MB
Binary file not shown.

LED_Matrix_Clock/square-alarm.wav

1.11 MB
Binary file not shown.

0 commit comments

Comments
 (0)