Overview
The Spirit Board project replicates a physical spirit board onto an LCD display. And just like the real board, a puck or planchette slides around. Messages can be fetched from AdafruitIO or loaded into a text file on the device. Customize the messages to delight, amaze, or haunt your friends and family.
The project artwork is designed to fit two display sizes: 480x320 and 320x240. Those are the sizes of the PyPortal Titano, and standard PyPortal. An alternative version is provided that runs on Feather microcontrollers connected to a TFT FeatherWing.
Page last edited October 16, 2024
Text editor powered by tinymce.
Project Setup PyPortal
Are you new to using CircuitPython? No worries, there is a full getting-started guide here.
Plug the device into your computer with a known good USB cable (not a charge-only cable). The device will appear to your computer in File Explorer or Finder (depending on your operating system) as a flash drive named CIRCUITPY. If the drive does not appear, you can install CircuitPython on your device and then return here.
Download the project files with the Download Project Bundle button below. Unzip the file and copy/paste the code.py and other project files to your CIRCUITPY drive using File Explorer or Finder (depending on your operating system).
Drive Structure
After copying the files, your drive should look like the listing below. It can contain other files as well, but must contain these at a minimum.
The code used by the different sized PyPortals is the same, but they do have different sized graphics. There are drive structure screenshots provided for each device below.
Code
The code.py file for the project is shown below.
The same code.py file is used for both the PyPortal and the PyPortal Titano, however the image assets differ.
PyPortal Standard Bundle
Use this Project Bundle button to download the standard PyPortal project bundle.
# SPDX-FileCopyrightText: 2024 Tim Cocks
#
# SPDX-License-Identifier: MIT
"""
SpiritBoard code.py
Standard PyPortal w/ 320x240 pixel display
Receive and display messages from the spirits.
"""
# pylint: disable=import-error, invalid-name
from os import getenv
import board
from digitalio import DigitalInOut
import adafruit_connection_manager
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_touchscreen
import adafruit_requests
from adafruit_io.adafruit_io import IO_HTTP
from spirit_board import SpiritBoard
display = board.DISPLAY
# Get WiFi details and Adafruit IO keys, ensure these are setup in settings.toml
# (visit io.adafruit.com if you need to create an account, or if you need your Adafruit IO key.)
ssid = getenv("CIRCUITPY_WIFI_SSID")
password = getenv("CIRCUITPY_WIFI_PASSWORD")
aio_username = getenv("ADAFRUIT_AIO_USERNAME")
aio_key = getenv("ADAFRUIT_AIO_KEY")
if None in [ssid, password, aio_username, aio_key]:
raise RuntimeError(
"WiFi and Adafruit IO settings are kept in settings.toml, "
"please add them there. The settings file must contain "
"'CIRCUITPY_WIFI_SSID', 'CIRCUITPY_WIFI_PASSWORD', "
"'ADAFRUIT_AIO_USERNAME' and 'ADAFRUIT_AIO_KEY' at a minimum."
)
# Initialize the touch overlay
touchscreen = adafruit_touchscreen.Touchscreen(
board.TOUCH_XL,
board.TOUCH_XR,
board.TOUCH_YD,
board.TOUCH_YU,
calibration=((6584, 59861), (9505, 57492)),
size=(board.DISPLAY.width, board.DISPLAY.height),
)
# Initialize the ES32SPI Coprocessor
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)
spi = board.SPI()
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
# connect to wifi network defined in settings.toml
print("Connecting to AP...")
try:
esp.connect_AP(ssid, password)
# Initialize a requests session
pool = adafruit_connection_manager.get_radio_socketpool(esp)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp)
requests = adafruit_requests.Session(pool, ssl_context)
# Initialize an Adafruit IO HTTP API object
io = IO_HTTP(aio_username, aio_key, requests)
except (RuntimeError, TypeError) as e:
print("could not connect to AP or AdafruitIO: ", e)
io = None
# initialize the SpiritBoard class
spirit_board = SpiritBoard(display)
# get messages from io or the local file
messages = spirit_board.get_messages(io)
# The planchette is already at the home position.
# Slide it to home again to make it jump, in order
# indicate the message is ready to be received.
spirit_board.slide_planchette(SpiritBoard.LOCATIONS["home"], delay=0.02, step_size=6)
# current message index
message_index = 0
while True:
# read the touch screen
p = touchscreen.touch_point
# if the display was touched
if p:
# write the message at the current index
spirit_board.write_message(messages[message_index], step_size=8)
# if there are more messages in the list inside of context
if message_index < len(messages) - 1:
# increment the message index
message_index += 1
else: # there are no more messages in the list
# reset the index to 0
message_index = 0
print("fetching next")
# fetch new messages
messages = spirit_board.get_messages(io)
# make the planchette jump to indicate messages are ready to display
spirit_board.slide_planchette(SpiritBoard.LOCATIONS["home"],
delay=0.02, step_size=6)
PyPortal Titano Bundle
Use this Porject Bundle button to download the PyPortal Titano project bundle.
# SPDX-FileCopyrightText: 2024 Tim Cocks
#
# SPDX-License-Identifier: MIT
"""
SpiritBoard code.py
PyPortal w/ 480x320 pixel display
Receive and display messages from the spirits.
"""
# pylint: disable=import-error, invalid-name
from os import getenv
import board
from digitalio import DigitalInOut
import adafruit_connection_manager
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_touchscreen
import adafruit_requests
from adafruit_io.adafruit_io import IO_HTTP
from spirit_board import SpiritBoard
display = board.DISPLAY
# Get WiFi details and Adafruit IO keys, ensure these are setup in settings.toml
# (visit io.adafruit.com if you need to create an account, or if you need your Adafruit IO key.)
ssid = getenv("CIRCUITPY_WIFI_SSID")
password = getenv("CIRCUITPY_WIFI_PASSWORD")
aio_username = getenv("ADAFRUIT_AIO_USERNAME")
aio_key = getenv("ADAFRUIT_AIO_KEY")
if None in [ssid, password, aio_username, aio_key]:
raise RuntimeError(
"WiFi and Adafruit IO settings are kept in settings.toml, "
"please add them there. The settings file must contain "
"'CIRCUITPY_WIFI_SSID', 'CIRCUITPY_WIFI_PASSWORD', "
"'ADAFRUIT_AIO_USERNAME' and 'ADAFRUIT_AIO_KEY' at a minimum."
)
# Initialize the touch overlay
touchscreen = adafruit_touchscreen.Touchscreen(
board.TOUCH_XL,
board.TOUCH_XR,
board.TOUCH_YD,
board.TOUCH_YU,
calibration=((6584, 59861), (9505, 57492)),
size=(board.DISPLAY.width, board.DISPLAY.height),
)
# Initialize the ES32SPI Coprocessor
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)
spi = board.SPI()
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
# connect to wifi network defined in settings.toml
print("Connecting to AP...")
try:
esp.connect_AP(ssid, password)
# Initialize a requests session
pool = adafruit_connection_manager.get_radio_socketpool(esp)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp)
requests = adafruit_requests.Session(pool, ssl_context)
# Initialize an Adafruit IO HTTP API object
io = IO_HTTP(aio_username, aio_key, requests)
except (RuntimeError, TypeError) as e:
print("could not connect to AP or AdafruitIO: ", e)
io = None
# initialize the SpiritBoard class
spirit_board = SpiritBoard(display)
# get messages from io or the local file
messages = spirit_board.get_messages(io)
# The planchette is already at the home position.
# Slide it to home again to make it jump, in order
# indicate the message is ready to be received.
spirit_board.slide_planchette(SpiritBoard.LOCATIONS["home"], delay=0.02, step_size=6)
# current message index
message_index = 0
while True:
# read the touch screen
p = touchscreen.touch_point
# if the display was touched
if p:
# write the message at the current index
spirit_board.write_message(messages[message_index], step_size=8)
# if there are more messages in the list inside of context
if message_index < len(messages) - 1:
# increment the message index
message_index += 1
else: # there are no more messages in the list
# reset the index to 0
message_index = 0
print("fetching next")
# fetch new messages
messages = spirit_board.get_messages(io)
# make the planchette jump to indicate messages are ready to display
spirit_board.slide_planchette(SpiritBoard.LOCATIONS["home"],
delay=0.02, step_size=6)
Page last edited October 16, 2024
Text editor powered by tinymce.
Project Setup TFT Featherwing
Are you new to using CircuitPython? No worries, there is a full getting-started guide here.
Plug the your device into your computer with a known good USB cable (not a charge-only cable). The device will appear to your computer in File Explorer or Finder (depending on your operating system) as a flash drive named CIRCUITPY. If the drive does not appear, you can install CircuitPython on your device and then return here.
Download the project files with the Download Project Bundle button below. Unzip the file and copy/paste the code.py and other project files to your CIRCUITPY drive using File Explorer or Finder (depending on your operating system).
Drive Structure
After copying the files, your drive should look like the listing below. It can contain other files as well, but must contain these at a minimum:
# SPDX-FileCopyrightText: 2024 Tim Cocks
#
# SPDX-License-Identifier: MIT
"""
SpiritBoard code.py
Feather ESP32-S3 or S2 + TFT Featherwing 3.5" 480x320 pixels
Receive and display messages from the spirits.
"""
# pylint: disable=import-error, invalid-name
from os import getenv
import displayio
import fourwire
import board
from digitalio import DigitalInOut
import adafruit_connection_manager
import wifi
import adafruit_requests
from adafruit_io.adafruit_io import IO_HTTP
from adafruit_hx8357 import HX8357 # TFT Featherwing 3.5" 480x320 display driver
import adafruit_tsc2007
from spirit_board import SpiritBoard
# Get WiFi details and Adafruit IO keys, ensure these are setup in settings.toml
# (visit io.adafruit.com if you need to create an account, or if you need your Adafruit IO key.)
ssid = getenv("CIRCUITPY_WIFI_SSID")
password = getenv("CIRCUITPY_WIFI_PASSWORD")
aio_username = getenv("ADAFRUIT_AIO_USERNAME")
aio_key = getenv("ADAFRUIT_AIO_KEY")
if None in [ssid, password, aio_username, aio_key]:
raise RuntimeError(
"WiFi and Adafruit IO settings are kept in settings.toml, "
"please add them there. The settings file must contain "
"'CIRCUITPY_WIFI_SSID', 'CIRCUITPY_WIFI_PASSWORD', "
"'ADAFRUIT_AIO_USERNAME' and 'ADAFRUIT_AIO_KEY' at a minimum."
)
# 3.5" TFT Featherwing is 480x320
displayio.release_displays()
DISPLAY_WIDTH = 480
DISPLAY_HEIGHT = 320
# Initialize TFT Display
spi = board.SPI()
tft_cs = board.D9
tft_dc = board.D10
display_bus = fourwire.FourWire(spi, command=tft_dc, chip_select=tft_cs)
display = HX8357(display_bus, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT)
display.rotation = 0
_touch_flip = (False, True)
# Initialize 3.5" TFT Featherwing Touchscreen
ts_cs_pin = DigitalInOut(board.D6)
i2c = board.I2C()
tsc = adafruit_tsc2007.TSC2007(i2c, irq=None)
try:
# Initialize a requests session
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
# Initialize an Adafruit IO HTTP API object
io = IO_HTTP(aio_username, aio_key, requests)
except (RuntimeError, TypeError) as e:
print("could not connect to AP or AdafruitIO: ", e)
io = None
# initialize the SpiritBoard class
spirit_board = SpiritBoard(display)
# get messages from io or the local file
messages = spirit_board.get_messages(io)
# The planchette is already at the home position.
# Slide it to home again to make it jump, in order
# indicate the message is ready to be received.
spirit_board.slide_planchette(SpiritBoard.LOCATIONS["home"],
delay=0.02, step_size=6)
# current message index
message_index = 0
while True:
# if the display was touched
if tsc.touched:
# write the message at the current index
spirit_board.write_message(messages[message_index], step_size=8)
# if there are more messages in the list inside of context
if message_index < len(messages) - 1:
# increment the message index
message_index += 1
else: # there are no more messages in the list
# reset the index to 0
message_index = 0
print("fetching next")
# fetch new messages
messages = spirit_board.get_messages(io)
# make the planchette jump to indicate messages are ready to display
spirit_board.slide_planchette(SpiritBoard.LOCATIONS["home"],
delay=0.02, step_size=6)
Page last edited October 16, 2024
Text editor powered by tinymce.
Loading Messages
There are 2 ways for the "spirits" to get their messages into the device: dynamically via AdafruitIO, or preloaded into a text file on the CIRCUITPY drive. The code provided by the project will attempt to fetch messages from AdafruitIO first, and if that doesn't succeed it will look for the spirit_messages.txt file locally to read messages from.
AdafruitIO Setup
To fetch messages from AdafruitIO, you need to add your WiFi details to settings.toml if you haven't already, and set up a SpiritBoard feed on AdafruitIO.
This guide page shows the process of creating and adding the necessary WiFi details plus AdafruitIO username and token to the settings.toml file. If you don't already have those in your settings.toml file from other projects then follow along with that page before returning here.
The environment variable names expected by this project are:
CIRCUITPY_WIFI_SSIDCIRCUITPY_WIFI_PASSWORDAIO_USERNAMEAIO_KEY
Open your browser to adafruit.io and login. Click the feeds link at the top. Next, click the 'New Feed' button near the top left. Enter the name "SpiritBoard" and optionally a description.
Local File
If you don't want to use WiFi, you can instead load your messages one per line into a file called spirit_messages.txt and put it at the root directory (\) of your CIRCUITPY drive. An empty spirit_messages.txt file is included in the project bundle so you be able to jump right into editing it (in any text editor) if you loaded the files from the bundle.
Example spirit_messages.txt contents:
hello world spirits are near i <3 circuitpython goodbye
Page last edited October 16, 2024
Text editor powered by tinymce.
Code Explanation
The code for this project is split into 3 main parts. Two of them are shared across all of the different supported devices, and the last part is more specific to the hardware used.
General components used by all versions
-
AnchoredTilegrid- This library provides a small wrapper class that extends TileGrid and provides the ability to place theTileGridrelative to points other than it's top left corner. This is useful because it allows us to place the planchette relative to the center of the window and not have to worry about the offset from the top left. See the documentation and library repository for more information and basic example usage of this class. -
SpiritBoard- This class holds everything needed to display the spirit board and planchette as well as slide the planchette around to write messages.
Hardware specific code.py and media
The display setup (or lack thereof), the different possible touch overlays, and the different methods used to connect to WiFi mean that this project has a few variations of the code.py file in order to support the different possible bits of hardware.
- PyPortal Titano - Built-in display with generic touch driver, esp32spi wifi
- PyPortal - Built-in display w/ smaller size and generic touch driver, esp32spi wifi
- Feather ESP32-S3 or S2 + TFT Featherwing - External display with tsc2007 touch driver, core wifi
The two different supported display sizes are 480x320 and 320x240. There are spirit board and planchette image files provided in the respective project bundles for each of those display sizes.
SpiritBoard Class
This class contains most of the shared code that all versions use. It is responsible for all of the graphics in this project. It extends the displayio.Group class, so that it can hold other displayio elements and can be set as the root_group on the display.
When initialized, it loads and displays the spirit board image in the background, and the planchette image in the foreground. It checks the height and width of the display object passed to the initialization to determine which assets should be used. If it finds the smaller of the two supported screen sizes it will also scale down the letter positions to fit the smaller sized board.
Write Message
The write_message() function is the highest level function, and is one of the functions called by the code.py files. It accepts a string message and will slide the planchette around to output the message. The logic in the function is responsible for breaking the message into its different words, then determining whether each word is one of the full words on the board, or needs to be output letter by letter. The movement of the planchette is delegated to the another of the primary functions for this class.
Slide Planchette
The slide_planchette() function handles moving the planchette from one location on the display to another. In order to do this it finds equally spaced points along the line between the points representing where we are moving from and moving to. The target location tuple is a required argument. There are optional arguments delay and step_size which work together to control how fast the planchette will slide. Delay is the time it will wait between steps, and step_size is how big of a step it will take with each movement.
Get Messages
get_messages() is responsible for getting the messages which will be displayed by the board. It uses two other functions descriptively named after the purpose they serve. It will first use sync_with_io() to attempt to fetch the messages from a "SpiritBoard" feed on AdafruitIO using the username and token stored in the settings.toml file. If that does not succeed then it will use read_local_messages_file() to fallback to looking for messages inside of a local file named spirit_messages.txt. See the Loading Messages page of this guide for instructions on how to load these messages.
Find the SpiritBoard class code file is embedded below. It is thoroughly commented with details explaining what each portion does.
# SPDX-FileCopyrightText: 2024 Tim Cocks
#
# SPDX-License-Identifier: MIT
"""
SpiritBoard helper class
"""
import math
import os
import random
import time
import displayio
# pylint: disable=import-error
from adafruit_anchored_tilegrid import AnchoredTileGrid
from adafruit_io.adafruit_io_errors import AdafruitIO_RequestError
class SpiritBoard(displayio.Group):
"""
DisplayIO Based SpiritBoard
Holds and manages everything needed to draw the spirit board and planchette, as well
as move the planchette around to output messages from the spirits.
"""
# Mapping of letters and words on the board to their pixel coordinates.
# Points are centered on the target letter.
# Words can contain a list of points, the planchette will move between them.
LOCATIONS = {"a": (42, 145), "b": (63, 115), "c": (97, 97), "d": (133, 85),
"e": (172, 78), "f": (207, 75), "g": (245, 74), "h": (284, 75),
"i": (319, 80), "j": (345, 85), "k": (375, 95), "l": (410, 111),
"m": (435, 140), "n": (78, 190), "o": (96, 162), "p": (122, 145),
"q": (149, 132), "r": (179, 123), "s": (208, 118), "t": (235, 116),
"u": (267, 116), "v": (302, 119), "w": (334, 130), "x": (368, 147),
"y": (393, 168), "z": (405, 194),
" ": (151, 201), "<3": (247, 20), "?": (162, 18), "&": (339, 18),
"home": (234, 246), "yes": [(26, 20), (82, 20)], "no": [(418, 20), (450, 20)],
"hello": [(20, 300), (123, 300)], "goodbye": [(314, 300), (456, 300)]}
# List of full words on the board (multi-character strings)
# used to know whether to parse the message
# one letter at a time, or with a full word.
FULL_WORDS = ["yes", "no", "hello", "goodbye", "home", "<3"]
def __init__(self, display):
"""
Create a SpiritBoard instance and put it in the displays root_group to make it visible.
:param displayio.AnyDisplay display: Display object to show the spirit board on.
"""
self._display = display
super().__init__()
# board image file
if display.width == 480 and display.height == 320:
self.spirit_board_odb = displayio.OnDiskBitmap("spirit_board_480x320.bmp")
elif display.width == 320 and display.height == 240:
self.spirit_board_odb = displayio.OnDiskBitmap("spirit_board_320x240.bmp")
self._convert_locations_for_small_screen()
self.spirit_board_tilegrid = displayio.TileGrid(
bitmap=self.spirit_board_odb, pixel_shader=self.spirit_board_odb.pixel_shader)
self.append(self.spirit_board_tilegrid)
# planchette image file
if display.width == 480 and display.height == 320:
self.planchette_odb = displayio.OnDiskBitmap("planchette_v1.bmp")
elif display.width == 320 and display.height == 240:
self.planchette_odb = displayio.OnDiskBitmap("planchette_v1_sm.bmp")
self.planchette_odb.pixel_shader.make_transparent(0)
self.planchette_tilegrid = AnchoredTileGrid(
bitmap=self.planchette_odb, pixel_shader=self.planchette_odb.pixel_shader)
# AnchoredTileGrid is used so that we can move the planchette
# relative to the cetner of the window.
self.planchette_tilegrid.anchor_point = (0.5, 0.5)
# move the planchette to it's home to start
self.planchette_tilegrid.anchored_position = SpiritBoard.LOCATIONS['home']
# append the planchette to the self Group instance
self.append(self.planchette_tilegrid)
# set the self Group instance to root_group, so it's shown on the display.
display.root_group = self
def _convert_locations_for_small_screen(self):
_x_ratio = 320/480
_y_ratio = 240/320
# 46x
print(_x_ratio, _y_ratio)
for key, value in self.LOCATIONS.items():
if isinstance(value, tuple):
_x, _y = value
self.LOCATIONS[key] = (int(_x * _x_ratio), int(_y * _y_ratio))
elif isinstance(value, list):
for i in range(len(value)):
_x, _y = value[i]
self.LOCATIONS[key][i] = (int(_x * _x_ratio), int(_y * _y_ratio))
@staticmethod
def dist(point_a, point_b):
"""
Calculate the distance between two points.
:param tuple point_a: x,y pair of the first point
:param point_b: x,y pair of the second point
:return: the distance between the two points
"""
return math.sqrt((point_b[0] - point_a[0]) ** 2 + (point_b[1] - point_a[1]) ** 2)
def slide_planchette(self, target_location, delay=0.1, step_size=4):
"""
Slide the planchette to the target location.
delay and step_size parameters can be used to control the speed of the sliding.
If the planchette is already at the target_location it will jump up slightly and
then return to the target location. This helps to clarify messages that contain
consecutive matching letters.
:param tuple target_location: x,y pair of the target location
:param float delay: length of time to sleep inbetween each movement step
:param int step_size: how big of a step to take with each movement.
:return: None
"""
# disable auto_refresh during sliding, we refresh manually for each step
self._display.auto_refresh = False
# current location
current_location = self.planchette_tilegrid.anchored_position
# get the distance between the two
distance = SpiritBoard.dist(current_location, target_location)
# if the planchette is already at the target location
if distance == 0:
# cannot slide to the location we're already at.
# slide up a tiny bit and then back to where we were.
self.slide_planchette((current_location[0], current_location[1] - 20), delay, step_size)
# update the current location to where we moved to
current_location = self.planchette_tilegrid.anchored_position
distance = SpiritBoard.dist(current_location, target_location)
# variables used to calculate where the next point
# between where we are at and where we are going is.
distance_ratio = step_size / distance
one_minus_distance_ratio = 1 - distance_ratio
# calculate the next point
next_point = (
round(one_minus_distance_ratio * current_location[0]
+ distance_ratio * target_location[0]),
round(one_minus_distance_ratio * current_location[1]
+ distance_ratio * target_location[1])
)
# print(current_location)
# print(next_point)
# update the anchored_position of the planchette to move it to
# the next point.
self.planchette_tilegrid.anchored_position = next_point
# refresh the display
self._display.refresh()
# wait for delay amount of time (seconds)
time.sleep(delay)
# while we haven't made it to the target location
while 0 < distance_ratio < 1:
# update current location variable
current_location = self.planchette_tilegrid.anchored_position
# calculate distance between new current location and target location
distance = SpiritBoard.dist(current_location, target_location)
# if we have arrived at the target location
if distance == 0:
# break out of the function
break
# distance ratio variables used to calculate next point
distance_ratio = step_size / distance
one_minus_distance_ratio = 1 - distance_ratio
# calculate the next point
next_point = (
round(one_minus_distance_ratio * current_location[0]
+ distance_ratio * target_location[0]),
round(one_minus_distance_ratio * current_location[1]
+ distance_ratio * target_location[1])
)
# if we have not arrived at the target location yet
if 0 < distance_ratio < 1:
# update the anchored position to move the planchette to the
# next point
self.planchette_tilegrid.anchored_position = next_point
# refresh the display
self._display.refresh()
# wait for delay amount of time (seconds)
time.sleep(delay)
# update the anchored position to move the planchette to the
# target_location. This is needed in-case we undershot
# the target location due to a step size that does not
# divide into the total distance evenly.
self.planchette_tilegrid.anchored_position = target_location
# refresh the display
self._display.refresh()
# re-enable auto_refresh in case any other parts of the program
# want to update the display
self._display.auto_refresh = True
def write_message(self, message, skip_spaces=True, step_size=6, delay=0.02):
"""
:param string message: The message to output with the planchette
:param skip_spaces: Whether to skip space characters
:param step_size: How big of a step to take with each movement
:param delay: How many seconds to sleep between each movement step
:return: None
"""
# ignore empty messages
if message == "":
return
# split the message on space to get a list of words
message_words = message.split(" ")
# loop over the words in the message
for index, word in enumerate(message_words):
print(f"index: {index}, word: {word}")
# send it to lowercase to get rid of capital letters
word = word.lower()
# if the current word is one of the full words on the board
if word in SpiritBoard.FULL_WORDS:
# if the word on the board has multiple points
if isinstance(SpiritBoard.LOCATIONS[word], list):
# loop over the points for the word
for location in SpiritBoard.LOCATIONS[word]:
print(f"sliding to: {location}")
# slide the planchette to each point
self.slide_planchette(location, delay=delay, step_size=step_size)
# pause at each point
time.sleep(0.25)
# if the word has only a single point
elif isinstance(SpiritBoard.LOCATIONS[word], tuple):
# slide the planchette to the point
self.slide_planchette(SpiritBoard.LOCATIONS[word],
delay=delay, step_size=step_size)
# pause at the point
time.sleep(0.5)
else: # the current word is not one of the full words
# go one character at a time
# loop over each character in the word
for character in word:
# if the character is in our locations mapping
if character in SpiritBoard.LOCATIONS:
# slide the planchette to the current characters location
self.slide_planchette(SpiritBoard.LOCATIONS[character],
delay=delay, step_size=step_size)
# pause after we arrive
time.sleep(0.5)
else:
# character is not in our mapping
print(f"Skipping '{character}', it's not on the board.")
# if we are not skipping spaces, and we are not done with the message
if not skip_spaces and index < len(message_words) - 1:
# handle the space
# slide the planchette to the empty space location.
self.slide_planchette(SpiritBoard.LOCATIONS[" "],
delay=delay, step_size=step_size)
# pause after we arrive
time.sleep(0.5)
# after we've shown the whole message
# slide the planchette back to it's home location
self.slide_planchette(SpiritBoard.LOCATIONS["home"], delay=delay, step_size=step_size)
@staticmethod
def sync_with_io(io) -> list:
"""
Fetch messages from AdafruitIO and store them in the context variable.
You must create the "SpiritBoard" feed object inside AdafruitIO for
this to succeed.
Will raise an exception if connecting or fetching failed.
:param io: The initialized adafruit IO object
:return: List of messages
"""
if io is None:
raise RuntimeError("No connection to AdafruitIO")
# fetch the latest data in the feed
incoming_message = io.receive_data("spiritboard")
# if it's multiple messages seperated by commas
if "," in incoming_message["value"]:
# split on the commas to seperate the messages
# and put them in context
messages = incoming_message["value"].split(",")
else: # it's only a single message
# set the single message into the context
messages = [incoming_message["value"]]
# print if successful
if len(messages) > 0:
print("io fetch success")
return messages
@staticmethod
def read_local_messages_file(shuffle=False) -> list:
"""
Read messages from the local spirit_messages.txt file on the CIRCUITPY drive.
Each message should be on its own line within that file.
:param boolean shuffle: Whether to shuffle the messages. Default is False
which will keep them in the same order they appear in the file.
:return: List of messages
"""
# if the spirit_messages.txt file exists
if "spirit_messages.txt" in os.listdir("/"):
# open the file
with open("/spirit_messages.txt", "r", encoding="utf-8") as f:
# split on newline and set the messages found into the context
messages = f.read().split("\n")
# if there are no messages
if len(messages) == 0:
# raise an error and tell the user to set some up
raise RuntimeError("Connection to adafruit.io failed, and there were "
"no messages in spirit_messages.txt. Enter your WIFI "
"credentials, aio username, and token in settings.toml, or add "
"messages to spirit_messages.txt.")
# if there are messages and we need to shuffle them
if shuffle:
# temporary list to hold them
shuffled_list = []
# while there are still messages in the context messages list
while len(messages) > 0:
# pop a randomly chosen message from the context and
# put it into the temporary list
shuffled_list.append(messages.pop(
random.randint(0, len(messages) - 1)))
# update the context list to the shuffled one
messages = shuffled_list
return messages
@staticmethod
def get_messages(io) -> list:
"""
Higher level get messages function. It will first attempt to
fetch the messages from Adafruit IO. If that doesn't work,
it will read them from the local spirit_messages.txt file.
:param io: The initialized adafruit IO object
:return: List of messages
"""
try:
return SpiritBoard.sync_with_io(io)
except (OSError, RuntimeError, AdafruitIO_RequestError) as e:
print(f"Caught Exception: {type(e)} - {e}.\nWill try again next time.\n"
"Falling back to spirit_messages.txt file.")
return SpiritBoard.read_local_messages_file()
Page last edited October 16, 2024
Text editor powered by tinymce.