code.py
press a button, get something soft
this is the full working code, unedited. copy it onto your
CIRCUITPY drive as code.py. the content
list near the top, messages and small ASCII art, is the part
you're meant to change, everything else can stay as it is.
what to edit
look for the block marked MESSAGES + ASCII ART. it's a
plain list, short phrases and a few small pieces of ASCII art (a
face, a tiny sun). add your own, remove ones that don't feel right,
reorder them, that's the whole interface. keep text short and
ASCII art narrow, the screen is small.
the full code
# tiny soft computer
# press the button once, then it runs on its own
# Feather RP2040 ThinkInk + 2.13" 212x104 Flexible Monochrome eInk (IL0373)
import time
import random
import gc
import board
import busio
import displayio
import terminalio
import digitalio
from fourwire import FourWire
import adafruit_il0373
from adafruit_display_text import label
# MESSAGES + ASCII ART
# Add, remove, or change these to make the computer your own.
content = [
"you can rest now",
"you are doing enough",
"it's okay to slow down",
"you don't have to earn rest",
"you are allowed to take up space",
"this moment is enough",
"be gentle with yourself today",
"you are not behind",
"know when to fold 'em",
"ready...set...go....",
"small steps still count",
"inhale.....exhale.......",
"you have what you need already",
"things do come back around",
"anything can be a poem - even this",
"you deserve softness too",
"you are allowed to change your mind",
"it's okay to ask for help",
"you don't have to be productive right now",
"your feelings make sense",
"you can start again tomorrow",
"you are worth taking care of",
"it's okay to not know yet",
"you can let this be easy",
"you are allowed to say no",
"you did not have to be perfect today",
"this is a good place to pause",
"you are allowed to feel proud",
"you can trust yourself here",
"it's okay to take up less than everything today",
"you are still growing",
# Little faces
"(^_^)",
"(._.)",
"(o_o)",
"(>_<)",
"(^.^)",
"(*_*)",
"(>.<)",
"(^o^)",
"(-_-)",
"(u_u)",
"(*_.)",
"(^-^)",
# A tiny sun
[
" \\ | / ",
"-- * --",
" / | \\ ",
"you are a beam of light",
],
]
# The display should not be updated more often than every
# 180 seconds, according to the display datasheet.
CONTENT_INTERVAL = 180
print("starting up")
displayio.release_displays()
# One button starts the computer.
# The button uses a pull-up resistor.
# False means pressed and True means not pressed.
button = digitalio.DigitalInOut(board.A2)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
# Set up the connection between the Feather and the e-ink display.
spi = busio.SPI(
board.EPD_SCK,
MOSI=board.EPD_MOSI,
MISO=None
)
epd_cs = board.EPD_CS
epd_dc = board.EPD_DC
epd_reset = board.EPD_RESET
epd_busy = board.EPD_BUSY
display_bus = FourWire(
spi,
command=epd_dc,
chip_select=epd_cs,
reset=epd_reset,
baudrate=1000000
)
time.sleep(1)
display = adafruit_il0373.IL0373(
display_bus,
width=212,
height=104,
rotation=90,
busy_pin=epd_busy,
black_bits_inverted=False,
color_bits_inverted=True,
swap_rams=True,
)
print("display ready")
print("display refresh time:", display.time_to_refresh, "seconds")
def wrap_text(text, max_chars=26):
# Break a long message into multiple lines
# so it fits on the small screen.
words = text.split(" ")
lines = []
current = ""
for word in words:
if len(current) + len(word) + 1 <= max_chars:
current = (
current + " " + word
if current
else word
)
else:
lines.append(current)
current = word
if current:
lines.append(current)
return "\n".join(lines)
# Tracks when the display last actually refreshed.
last_refresh_time = None
def draw_screen(lines):
# Draws one screen and refreshes the display.
# Waits here if needed so the display never refreshes
# sooner than CONTENT_INTERVAL.
global last_refresh_time
if last_refresh_time is not None:
elapsed = time.monotonic() - last_refresh_time
remaining = CONTENT_INTERVAL - elapsed
if remaining > 0:
wait_with_countdown(remaining)
g = displayio.Group()
# Create a white background.
main_bitmap = displayio.Bitmap(
display.width,
display.height,
2
)
main_palette = displayio.Palette(2)
main_palette[0] = 0xFFFFFF
main_palette[1] = 0x000000
main_sprite = displayio.TileGrid(
main_bitmap,
pixel_shader=main_palette
)
g.append(main_sprite)
# Add each piece of text to the screen.
for text_content, scale, y_offset in lines:
text_area = label.Label(
terminalio.FONT,
text=text_content,
scale=scale,
color=0x000000
)
text_area.anchor_point = (0.5, 0.5)
text_area.anchored_position = (
display.width // 2,
display.height // 2 + y_offset
)
g.append(text_area)
display.root_group = g
try:
print("refreshing display...")
display.refresh()
print("refresh complete")
last_refresh_time = time.monotonic()
return True
except RuntimeError as error:
# This is a safety net if the display is not ready.
print("display wasn't ready:", error)
last_refresh_time = time.monotonic()
return False
# Tracks whether we've shown a message yet.
first_message_shown = False
def show_message(message):
# Show one message or drawing on the screen.
global first_message_shown
if first_message_shown:
print("showing next:")
else:
print("showing:")
first_message_shown = True
print("--------------------------------")
if isinstance(message, list):
for line in message:
print(line)
else:
print(message)
print("--------------------------------")
# A list means the content has multiple lines,
# like the little sun.
if isinstance(message, list):
lines = []
start_y = -14
for line in message:
lines.append(
(line, 1, start_y)
)
start_y += 14
return draw_screen(lines)
# Regular messages are wrapped automatically.
wrapped = wrap_text(message)
return draw_screen([
(wrapped, 1, 0)
])
def show_intro():
# This is the screen shown before the button is pressed.
return draw_screen([
("tiny soft computer", 1, -14),
("press button\ngive it a moment ...", 1, 18),
])
def shuffle_list(items):
# Put the messages into a random order.
for i in range(len(items) - 1, 0, -1):
j = random.randint(0, i)
items[i], items[j] = (
items[j],
items[i]
)
# Add special messages here.
# The number is the number of seconds into the wait.
#
# The Serial Monitor will still show every 10 seconds.
# These messages can happen at any time.
waiting_messages = {
20: "still here",
35: "a little more patience",
50: "taking its sweet time",
60: "thinking....",
70: "still thinking",
90: "halfway there",
110: "a little longer",
130: "nearly there",
145: "just hanging out",
160: "almost there",
170: "so little time",
}
def wait_with_countdown(total_seconds):
# Wait while showing progress in the Serial Monitor.
# A regular update appears every 10 seconds.
# Special messages can appear at other times.
# The button is also checked while waiting.
start_time = time.monotonic()
next_ten_second_update = 10
printed_messages = []
button_was_pressed = False
while True:
elapsed = time.monotonic() - start_time
# Check the button while waiting.
if not button.value:
if not button_was_pressed:
print("button pressed, just a few seconds...")
button_was_pressed = True
else:
button_was_pressed = False
# Print every 10 seconds.
if elapsed >= next_ten_second_update:
if next_ten_second_update in waiting_messages:
print(
next_ten_second_update,
"seconds -",
waiting_messages[next_ten_second_update]
)
else:
print(
next_ten_second_update,
"seconds"
)
next_ten_second_update += 10
# Print special messages that happen between
# the regular 10-second updates.
for message_time in waiting_messages:
if (
message_time not in printed_messages
and elapsed >= message_time
and message_time % 10 != 0
):
print(
message_time,
"seconds -",
waiting_messages[message_time]
)
printed_messages.append(message_time)
# Stop when the full wait is complete.
if elapsed >= total_seconds:
break
time.sleep(0.05)
def run_forever():
# Once the button is pressed, the computer
# takes over and keeps running by itself.
print("")
print("tiny soft computer is running")
print("changing every", CONTENT_INTERVAL, "seconds")
print("")
while True:
# Make a copy so we can shuffle the order
# without changing the original list.
order = list(content)
shuffle_list(order)
# Show every item once before making
# a new random order.
for item in order:
show_message(item)
# Tidy up memory now and then.
gc.collect()
# Start again with a new random order.
print("finished this set, reshuffling...")
# Show the intro when the computer starts.
show_intro()
print("")
print("ready, press the button to begin")
print("")
# The button only needs to be pressed once.
# After that run_forever() takes over.
while True:
if not button.value:
print("button pressed, just a few seconds...")
time.sleep(0.5)
run_forever()
time.sleep(0.05) alternative: OLED version
if you're using a monochrome OLED instead of the eInk display (see resources for why and how to swap), this is the working code for it. simpler than the eInk version, since OLED has no refresh-rate limit to design around.
# tiny soft computer (OLED version)
# press the button, get a random message, instantly
# Feather RP2040 ThinkInk + Monochrome 0.96" 128x64 OLED (STEMMA QT)
import time
import random
import board
import displayio
import terminalio
import digitalio
import i2cdisplaybus
import adafruit_displayio_ssd1306
from adafruit_display_text import label
# Why this file looks different from the eInk version:
# the eInk display can only redraw once every 180 seconds, so that
# version is built around a shuffle-and-wait loop to work around that.
# this OLED has no such limit, it refreshes instantly, so none of that
# complexity is needed here. press the button, see something new,
# right away. same idea, simpler code, because the hardware allows it.
# MESSAGES + ASCII ART
# Copy your own list from the eInk version's code.py, or start here.
# Add, remove, or change these to make the computer your own.
content = [
"you can rest now",
"you are doing enough",
"it's okay to slow down",
"you don't have to earn rest",
"you are allowed to take up space",
"this moment is enough",
"be gentle with yourself today",
"you are not behind",
"know when to fold 'em",
"ready...set...go....",
"small steps still count",
"inhale.....exhale.......",
"you have what you need already",
"things do come back around",
"anything can be a poem - even this",
"you deserve softness too",
"you are allowed to change your mind",
"it's okay to ask for help",
"you don't have to be productive right now",
"your feelings make sense",
"you can start again tomorrow",
"you are worth taking care of",
"it's okay to not know yet",
"you can let this be easy",
"you are allowed to say no",
"you did not have to be perfect today",
"this is a good place to pause",
"you are allowed to feel proud",
"you can trust yourself here",
"it's okay to take up less than everything today",
"you are still growing",
# Little faces
"(^_^)",
"(._.)",
"(o_o)",
"(>_<)",
"(^.^)",
"(*_*)",
"(>.<)",
"(^o^)",
"(-_-)",
"(u_u)",
"(*_.)",
"(^-^)",
# A tiny sun
[
" \\ | / ",
"-- * --",
" / | \\ ",
"you are a beam of light",
],
]
print("starting up")
displayio.release_displays()
# One button, same as the eInk version.
# The button uses a pull-up resistor.
# False means pressed and True means not pressed.
button = digitalio.DigitalInOut(board.A2)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
# Set up the OLED over the board's built-in STEMMA QT connector.
# This is I2C, not SPI, so there's no busy pin and no FourWire setup,
# just plug the STEMMA QT cable in and go.
#
# most SSD1306 OLEDs answer at address 0x3c, but some units are set
# to 0x3d instead (this is set by a jumper on the OLED board itself,
# so it varies unit to unit, not something we can predict ahead of
# time for a batch of kits). rather than guessing, scan the bus and
# use whichever address actually responds.
i2c = board.STEMMA_I2C()
while not i2c.try_lock():
pass
found_addresses = i2c.scan()
i2c.unlock()
if 0x3C in found_addresses:
oled_address = 0x3C
elif 0x3D in found_addresses:
oled_address = 0x3D
else:
print("no OLED found on the I2C bus.")
print("check that the STEMMA QT cable is fully seated on both ends.")
raise RuntimeError("no OLED detected at 0x3c or 0x3d")
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=oled_address)
display = adafruit_displayio_ssd1306.SSD1306(
display_bus,
width=128,
height=64,
)
print("display ready")
def wrap_text(text, max_chars=20):
# Break a long message into multiple lines so it fits
# on this smaller 128px-wide screen.
words = text.split(" ")
lines = []
current = ""
for word in words:
if len(current) + len(word) + 1 <= max_chars:
current = (
current + " " + word
if current
else word
)
else:
lines.append(current)
current = word
if current:
lines.append(current)
return "\n".join(lines)
def draw_screen(lines):
# Draws one screen. No waiting here, and no refresh() call
# either, this display auto-refreshes the instant root_group
# is set. That's the whole benefit of OLED over eInk.
g = displayio.Group()
# Create a white background.
main_bitmap = displayio.Bitmap(
display.width,
display.height,
2
)
main_palette = displayio.Palette(2)
main_palette[0] = 0x000000
main_palette[1] = 0xFFFFFF
main_sprite = displayio.TileGrid(
main_bitmap,
pixel_shader=main_palette
)
g.append(main_sprite)
# Add each piece of text to the screen.
for text_content, scale, y_offset in lines:
text_area = label.Label(
terminalio.FONT,
text=text_content,
scale=scale,
color=0xFFFFFF
)
text_area.anchor_point = (0.5, 0.5)
text_area.anchored_position = (
display.width // 2,
display.height // 2 + y_offset
)
g.append(text_area)
display.root_group = g
def show_message(message):
# Show one message or drawing on the screen.
print("")
print("--------------------------------")
if isinstance(message, list):
print("showing:")
for line in message:
print(line)
else:
print("showing:", message)
print("--------------------------------")
# A list means the content has multiple lines,
# like the little sun. this screen is shorter than the eInk
# one, so lines are spaced a bit tighter (12px instead of 14px).
if isinstance(message, list):
lines = []
start_y = -18
for line in message:
lines.append(
(line, 1, start_y)
)
start_y += 12
return draw_screen(lines)
# Regular messages are wrapped automatically if too long.
wrapped = wrap_text(message)
return draw_screen([
(wrapped, 1, 0)
])
def show_intro():
# This is the screen shown before the button is pressed.
return draw_screen([
("tiny soft computer", 1, -12),
("press the button", 1, 8),
])
# Show the intro when the computer starts.
show_intro()
print("")
print("ready, press the button")
print("")
# Every press shows a new random message, right away.
# No shuffling, no waiting, no forever-loop needed, the display
# can keep up with however fast someone wants to press it.
while True:
if not button.value:
print("button pressed")
# Wait for the button to be released, so holding it down
# doesn't trigger the computer more than once.
while not button.value:
time.sleep(0.01)
message = random.choice(content)
show_message(message)
time.sleep(0.05) libraries this needs
from the CircuitPython Library Bundle, matching your board's version:
for the eInk version (code.py above)
adafruit_il0373.mpyadafruit_display_text(folder)
for the OLED version
adafruit_displayio_ssd1306.mpyadafruit_display_text(folder), same one, shared by both versions
full install steps are on the educator setup guide.