-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcode.py
More file actions
184 lines (145 loc) · 5.51 KB
/
code.py
File metadata and controls
184 lines (145 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# code.py — XKCD E-Paper Reader — Main entry point
# XIAO ESP32-S3 + DEPG0750RNU790F30HP (UC8179, 800x480 BWR)
# CircuitPython 10.x
import os
import time
import gc
import alarm
import busio
import board
import wifi
import socketpool
import ssl
import adafruit_requests
import adafruit_connection_manager
from epaper import UC8179
from image_utils import fetch_xkcd_info, download_png, decode_and_process
from text_render import render_text
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
# Set to a small value (e.g. 30) for testing the wake/sleep cycle
SLEEP_SECONDS = 86400 # 24 hours
# Pin assignments (XIAO ESP32-S3)
_PIN_CS = board.D3 # GPIO4
_PIN_DC = board.D2 # GPIO3
_PIN_RST = board.D1 # GPIO2
_PIN_BUSY = board.D0 # GPIO1
_PIN_BTN = board.D5 # GPIO6 (active LOW, internal pull-up)
# SPI pins are fixed on XIAO ESP32-S3: SCK=D8/GPIO7, MOSI=D10/GPIO9
_SPI_CLK = board.D8
_SPI_MOSI = board.D10
# Persistent state files
_FILE_COMIC = "/last_comic.txt"
_FILE_ALT = "/last_alt.txt"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _read_file(path: str, default: str = "") -> str:
try:
with open(path, "r") as f:
return f.read().strip()
except OSError:
return default
def _write_file(path: str, content: str):
with open(path, "w") as f:
f.write(content)
def _make_display() -> UC8179:
spi = busio.SPI(clock=_SPI_CLK, MOSI=_SPI_MOSI)
# Run SPI at 4 MHz (UC8179 max is typically 20 MHz; conservative here)
while not spi.try_lock():
pass
spi.configure(baudrate=4_000_000, phase=0, polarity=0)
spi.unlock()
return UC8179(spi, _PIN_CS, _PIN_DC, _PIN_RST, _PIN_BUSY)
def _show_bitmap(bmp: bytearray):
"""Initialise display, push bitmap, refresh, then sleep the panel."""
display = _make_display()
display.init_display()
display.display_frame(bmp)
display.refresh()
display.sleep()
gc.collect()
def _connect_wifi():
"""Connect to WiFi using credentials from settings.toml."""
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
if not ssid:
raise RuntimeError("CIRCUITPY_WIFI_SSID not set in settings.toml")
print(f"Connecting to WiFi SSID: {ssid}")
wifi.radio.connect(ssid, password)
print(f"Connected, IP: {wifi.radio.ipv4_address}")
def _make_requests():
pool = socketpool.SocketPool(wifi.radio)
ssl_ctx = ssl.create_default_context()
manager = adafruit_connection_manager.get_radio_connection_manager(wifi.radio)
return adafruit_requests.Session(pool, ssl_ctx)
def _disconnect_wifi():
try:
wifi.radio.stop_station()
except Exception:
pass
def _enter_deep_sleep():
"""Sleep until scheduled time OR button press (whichever comes first)."""
print(f"Entering deep sleep for {SLEEP_SECONDS} s (or until button press)")
time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + SLEEP_SECONDS)
pin_alarm = alarm.pin.PinAlarm(pin=_PIN_BTN, value=False, pull=True)
alarm.exit_and_deep_sleep_until_alarms(time_alarm, pin_alarm)
# Never reaches here
# ---------------------------------------------------------------------------
# Wake handlers
# ---------------------------------------------------------------------------
def _handle_button_wake():
"""Display the stored alt-text, then return to deep sleep."""
print("Wake: button pressed — showing alt-text")
alt_text = _read_file(_FILE_ALT, default="No alt-text stored yet.")
try:
bmp = render_text(alt_text)
_show_bitmap(bmp)
except MemoryError:
print("MemoryError rendering alt-text — skipping display update")
_enter_deep_sleep()
def _handle_time_wake():
"""Fetch latest XKCD comic; update display if new; return to deep sleep."""
print("Wake: scheduled — checking for new comic")
last_num = int(_read_file(_FILE_COMIC, default="0"))
try:
_connect_wifi()
requests = _make_requests()
info = fetch_xkcd_info(requests)
num = info["num"]
img_url = info["img"]
alt_text = info["alt"]
print(f"Latest comic: #{num}")
if num != last_num:
print(f"New comic #{num} — downloading image")
try:
png = download_png(requests, img_url)
gc.collect()
bmp = decode_and_process(png)
del png
gc.collect()
_show_bitmap(bmp)
del bmp
gc.collect()
_write_file(_FILE_COMIC, str(num))
_write_file(_FILE_ALT, alt_text)
print("Display updated and state saved")
except MemoryError:
print("MemoryError during image processing — skipping display update")
else:
print("No new comic — nothing to display")
except Exception as e:
print(f"Error during fetch/update: {e}")
finally:
_disconnect_wifi()
_enter_deep_sleep()
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
wake = alarm.wake_alarm
if isinstance(wake, alarm.pin.PinAlarm):
_handle_button_wake()
else:
# First boot (wake is None) or TimeAlarm
_handle_time_wake()