In an increasingly digital world, analog tech is making a comeback. And by "analog tech, " I don't mean low-tech as much as early-tech. There are still digital bits inside, but enough physical and mechanical pieces poking out to cause one to yearn for halcyon days of yore.
My favorite example of analog tech is the humble split-flap display.
A split-flap display is an electromechanical device that consists of several modules (sometimes called bits or cells). Each module contains characters, numbers, graphics, or other information fixed onto flaps inside of the module. The flaps are arranged around a drum and a motor rotates the drum until the desired flap is reached.
Split-flap displays were most-often found in airports and railway stations (and can still be!) but you might spot the technology in a split-flap clock. Or you might stumble across them in the wild, like I did at the Boston Public Market a few months ago.
You can build your own spit-flap display, and there are a ton of great instructional videos on YouTube detailing how to do this. Alternatively, there are companies on the market who sell full-featured displays, like Vestaboard.
The Vestaboard, and other displays like it, are not cheap, but given the components and tech in here, you might spend just as much money building one yourself if you wanted to replicate the number of modules on one of these displays.
I acquired a Vestaboard for my office a few months ago, and given my desire to cellular-connect everything, I figured this technology was a great way to show-off how the Notecard and Notehub.io could be used to manage and control a fleet of split-flap displays.
In this article, I'll walk you through a full solution, including how to control a Vestaboard using an ESP32 over local Wi-FI, and the Notecard and Notehub.io to synchronize the text on the board across a fleet of devices using environment variables.
This project is built around a hypothetical entity, "Blues Railway, " which manages several train stations throughout the United States. Each station contains a set of analog split-flap displays that provide visitors with real-time departure information like city, track, time, and whether there is a delay. Departure information is set across the station using Fleet-level Notehub environment variables. Variable changes are sent to the Notecard and delivered to an ESP32. The ESP32 then uses its on-board Wi-Fi connection to communicate with the signs in the facility and update the displayed information.
Here's a ~4 minute demo of the final product, in action.
Let’s start by looking at the hardware.
NOTE: This project’s firmware and web application are both open source and available on GitHub.Hardware
The hardware for this project starts with the Vestaboard and includes both Cellular and Wi-Fi communication.
Cellular connectivity is facilitated by the Notecard and along with Notehub.io. If you've not yet heard of the Notecard, it's a cellular and GPS/GNSS-enabled device-to-cloud data-pump that comes with 500 MB of data and 10 years of cellular.
The Notecard itself is a small 30x35 system on module with an M.2 connector. To make integration into an existing prototype or project easier, Blues also makes a series of host boards called Notecarriers. For this project I went with a Notecarrier-F because it includes a set of headers for any Feather-compatible MCU.
For my build, Wi-Fi is also needed for local communication with the Vestaboard. I peeked inside of the Vestaboard and I know there's a Raspberry Pi CM4 serving as the brains of the thing that I could have explored connecting to directly, but since Vestaboard provides a local network API for controlling the board and I wasn't in a warranty-voiding mood, I opted to take a hands-off approach and go with an ESP32.
The ESP32 I chose was the FeatherS2 from Unexpected Maker. It has an onboard ESP32-S2 with 12MB of Flash, a ton of RAM, USB-C connector and a dotstar neopixel, so there's a lot to love about this board. It also runs CircuitPython like a champ, which as you'll see shortly, greatly simplified the firmware I created for this project.
The FeatherS2 is, as the name indicates, Feather-compatible, so all I needed to do to get started was connect my Notecard to the Notecarrier F, plug in the Feather S2, connect an antenna to the Notecard, and power the whole thing up.
Vestaboard provides a mobile app, cloud API and a local API for controlling a board on your local network. Using the local approach requires a small firmware update to the board, and you'll need to request access here to enable it and get a token for local access.
Once your board has been updated and the Vestaboard team sends you your enablement token, you'll need to send a cURL request to the board to get an API key for future requests.
curl -X POST -H "X-Vestaboard-Local-Api-Enablement-Token: <token>" http://vestaboard.local:7000/local-api/enablement
If successful, that request will return the key you need.
{"message":"Local API enabled","apiKey":"MDJEMEZEODEtRjNFMS00QUNFLUI0MzAtNjJCQkMyNUJDOUI5Cg"}
Setting Up the Cloud BackendNext, let's set-up the cloud backend for sending updates to the Vestaboard.
One of the great things about the Notecard is it knows how to send data to its cloud backend, Notehub.io, out of the box. Notehub is a hosted service designed to connect to Notecard devices and synchronize data.
If you’re following along and want to build this project yourself, you’ll need to set up an account on Notehub.io, and create a new project.
You’ll learn more about Notehub throughout this article, but now that you have the backend set up, let’s next look at the code that makes the project work.
Writing the FirmwareThe firmware for this project is responsible for obtaining board updates from the Notecard and Notehub.io, formatting that text for the Vestaboard, and communicating with the Vestaboard over local Wi-Fi.
The full source code needed to make that happen is available on GitHub but I’ll show off the most important parts here. I used CircuitPython for the app, with the FeatherS2 as the target. (And here are the instructions for getting all of that running on your device.)
Set Up Your Secrets
First things first, you'll want to create a secrets.py
file to hold sensitive information, specifically your Wi-Fi SSID, password, and the Vestaboard API key you obtained earlier.
secrets = {
'ssid': 'your_ssid',
'passwd': 'your_pw',
'vestaboard_url': 'vestaboard_local_api',
'vestaboard_key': 'vestaboard_key'
}
Connect to Wi-Fi
In the main code.py
file for your app, the first thing you want to do is connect the ESP32 to your local network using the SSID and password specified in secrets,py
.
After a successful Wi-Fi connection, create a socket pool for re-use and a session object for making requests to the Vestaboard.
import wifi
import adafruit_requests
import socketpool
from secrets import secrets
wifi.radio.connect(ssid=secrets['ssid'], password=secrets['passwd'])
print("IP addr:", wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)
request = adafruit_requests.Session(pool)
Set Up the Vestaboard
Once you're connected to Wi-Fi, you can make a first request to the Vestaboard using its local API. The API expects POST requests and a two-dimensional array for the request body. The provided array must itself contain six arrays of 22 elements each. Each array corresponds to a row on the board, and each element a character "bit."
To instruct the Vestaboard to display one of its supported characters, each element should be set to a character code between 0 and 71, as detailed here. For example, if I want the display to spin to an R for an element, I send 18
in the request. If I want the color green, I send 66
. Every request to the Vestaboard must include a complete 6x22 array, with zeros for any bits you do not intend to set.
To make working with the Vestaboard a bit easier, I created a helper file, vestaboard.py
and added variables to hold a few of the various board states. When I need to send one of these values to the Vestaboard, I simply need to str()
them to get the request payload I need.
board_header = [
[67, 67, 67, 67, 2, 12, 21, 5, 19, 0, 0, 18, 1, 9, 12, 23, 1, 25, 67, 67, 67, 67],
[0, 0, 3, 9, 20, 25, 0, 0, 20, 18, 1, 3, 11, 0, 0, 20, 9, 13, 5, 0, 0, 0]
]
blank_row = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
blank_board = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
base_board = [
board_header[0],
board_header[1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
The first request I send to the Vestaboard is contained in the base_board
array. In my main code.py I can send a request to set up the board with a few header rows:
board_state = str(vestaboard.base_board)
board_headers = {"X-Vestaboard-Local-Api-Key": secrets['vestaboard_key']}
response = req.post(secrets['vestaboard_url'], headers=board_headers, data=board_state)
Which resolves to the following:
Configure the Notecard
The next step is to configure the Notecard. The Notecarrier F provides a connection between a host and the Notecard over I2C, so I'll use that interface and configure the Notecard to have a continuous connection to Notehub and to assign this Notecard to my Analog Signage project via its ProductUID (com.blues.nf5
)
import board
import notecard
import json
i2c = board.I2C()
card = notecard.OpenI2C(i2c, 0, 0, debug=False)
req = {"req": "hub.set", "product": "com.blues.nf5", "mode": "continuous", "sync": True}
card.Transaction(req)
Working with Environment VariablesWith the Notecard configured and connection to the Vestaboard in place, it's time to connect the cloud to my board. One powerful feature of the Notecard is its ability communicate in two directions. That is, the Notecard can send your data up to the cloud, but it can also retrieve data from the cloud. And one of the most useful things you can do with this ability is to work with environment variables.
Environment variables allow you to synchronize state and settings across fleets of devices of any size. And for this project, environment variables allow a station manager of Blues Railway to update departure information across a fleet of split-flap displays in a single shot.
In keeping with the train station theme, I configured my app to use four environment variables named line_1
through line_4
with each corresponding to a departure notification and one of the four open rows on my Vestaboard.
Each environment variable value is a JSON object in the following format {"city":"SLC","track":"02","time":"09:55","delay":"true"}
. The firmware in this repo expects keys like city
, track
, time
and delay
, while the values can be varied. When all four variables are set in the Notehub UI, it will look something like this.
Back in the firmware, my running host regularly polls the Notecard for environment variable updates and, when the Notecard has new information to share, pulls those off the device using the env.get
API. The string values are parsed into JSON and an application_state
dictionary is updated with the new values, and a boolean indicating the update.
application_state = {
"lines": {},
"variables_updated": False
}
def fetch_environment_variables():
req = {"req": "env.get", "names": ["line_1", "line_2", "line_3", "line_4"]}
rsp = card.Transaction(req)
if rsp is not None:
env_vars = rsp["body"]
for key in env_vars:
try:
line_var = json.loads(env_vars[key])
if key not in application_state["lines"]:
application_state["lines"][key] = line_var
application_state["variables_updated"] = True
elif application_state["lines"][key] != line_var:
application_state["lines"][key] = line_var
application_state["variables_updated"] = True
except ValueError:
print("Invalid JSON object in environment variable")
Before I can update the Vestaboard with these new values, however, I need to create the 6x22 array for the board, and map the characters in the environment variables into character codes the Vestaboard understands. To help with this, I created a character codes map in my vestaboard.py
helper.
character_codes = {
'blank': 0,
'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6,
'G': 7, 'H': 8, 'I': 9, 'J': 10, 'K': 11, 'L': 12,
'M': 13, 'N': 14, 'O': 15, 'P': 16, 'Q': 17, 'R': 18,
'S': 19, 'T': 20, 'U': 21, 'V': 22, 'W': 23, 'X': 24,
'Y': 25, 'Z': 26, '1': 27, '2': 28, '3': 29, '4': 30,
'5': 31, '6': 32, '7': 33, '8': 34, '9': 35, '0': 36,
'!': 37, '@': 38, '#': 39, '$': 40, '(': 41, ')': 42,
'-': 44, '+': 46, '&': 47, '=': 48, ';': 49, ':': 50,
'\'': 52, '\"': 53, '%': 54, ',': 55, '.': 56, '/': 59,
'?': 60, 'deg': 62, 'red': 63, 'orange': 64, 'yellow': 65, 'green': 66,
'blue': 67, 'violet': 68, 'white': 69, 'black': 70, 'filled': 71
}
Then, I created a function to enumerate over the environment variables and set-up each line for the board. The first column will be red or green depending on whether the departure on that line is delayed, while the city, track, and departure time show in their respective parts of the row. All other columns are filled with 0s to indicate an empty bit.
def map_env_vars():
updated_board = vestaboard.base_board
for key, value in application_state["lines"].items():
row_state = []
key_index = int(key[-1]) + 1
if value['delay'] is 'true':
row_state.extend([vestaboard.character_codes['red'], 0])
else:
row_state.extend([vestaboard.character_codes['green'], 0])
for _, char in enumerate(value['city']):
row_state.append(vestaboard.character_codes[char])
row_state.extend([0, 0, 0])
for _, char in enumerate(value['track']):
row_state.append(vestaboard.character_codes[char])
row_state.extend([0, 0, 0, 0, 0])
for _, char in enumerate(value['time']):
row_state.append(vestaboard.character_codes[char])
row_state.extend([0,0,0,0,0])
updated_board[key_index] = row_state
return updated_board
Finally, I'll send another request to the board to display the full set of information.
board_headers = {"X-Vestaboard-Local-Api-Key": secrets['vestaboard_key']}
response = req.post(secrets['vestaboard_url'], headers=board_headers, data=str(board_data))
Once updated, the board will look like this.
Of course, most of the fun of a split-flap display is watching (and hearing!) it in action. There's a full demo video above where you can hear the display in all it's click-clack glory, and here are a few gifs that show off the board.
First, the initial set-up.
Next, loading the first set of environment variables from the Notecard and Notehub.
Finally, if I update the environment variables after the display has loaded them and change some of the details for each line, I get a slick partial update of the board.
I only used a single split-flap display for this project, but the beauty of the Notecard and Notehub is that I could add more displays to my application, place them in my fleet and control them all at once.
If you’re interested in trying this yourself, you’ll want to start with this project’s README on GitHub. From there, you’ll want to pick up a Notecard and Notecarrier F, get a Vestaboard (or two!) and an ESP32. Then flash the project’s firmware and start orchestrating some analog tech with Cellular!
If you run into any issues, feel free to reach out on the Blues Wireless forum for help.
Comments