We live in a world of amazing display technologies. LED, OLED, LCD, HD, 4K, 8K, or whatever, our screens are higher resolution, and higher fidelity than they've ever been before. But hi-res and hi-fi means high power, and sometimes a low(er)-tech and low-power solution is the way to go.
Especially if you want to power your project with a battery. And for displaying information, the best example of this is the e-Ink or e-Paper display. First made popular in the early 2000s in e-readers like the Amazon Kindle and others like it, the e-Ink or e-Paper display is so named because of its visual similarity to ink on paper and the absence of independent lightning in most uses of the technology. E-Ink is far from low-tech and is a marvel of engineering, but it is known for being low-power.
As a low-power technology for text and images, I think e-Ink is a perfect choice for the kinds of displays that you often find around office buildings, break rooms, or around a home. These devices can be battery powered and, when paired with a low-power MCU, can update text or load images as-needed.
Even better still, when paired with the Cellular-powered Blues Notecard and Notehub.io, we can extend the low-power nature of this solution to the cloud and gain the ability to manage a number of e-Ink displays at the same time. The end result: connected, dynamic digital signage without buying a fleet of smart TVs.
In this article, I'll walk through a simple, low-cost and low-power solution for connected digital signage using the Notecard, Notehub.io and e-Ink. The hardware consists of a Notecard, Swan, Notecarrier B, and an inexpensive e-Ink display. The firmware responds to Notehub environment variable changes and uses those values to update the text on the display, or show a bitmap image. Environment variables can be set on a device or at the fleet level to update any number of connected e-Ink displays at the same time.
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 e-Ink display. There are no shortage of great displays available in one, two, three, or more colors, and I selected the Adafruit 2.13" Tri-color FeatherWing for this project. It supports three colors, has a 250x122 pixel screen, includes a MicroSD card slot for images, and as a FeatherWing, has a socket on the back for a Feather-compatible MCU.
For that MCU, I chose the Blues Wireless Swan. Swan is an STM32-based MCU and uses an STM32L4 chip, which is tuned for low-power applications. It comes in a Feather form-factor, but also has castellated edges that bring out 55 GPIOs if you want to solder it onto a custom board.
Cellular connectivity is facilitated by the Notecard along with Notehub.io on the backend. 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 service.
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 B because it's a small-profile board that's perfect for my e-Ink project, and I can use the onboard Qwiic connecter for hooking the Notecard up to the Swan.
Finally, I 3D printed a stand for each device using this design from the Ruiz Brothers and used some M2.5 machine screws to secure the display to the stand. Then, after connecting the Notecard to the Notecarrier B via its m.2 connector, I plugged the Swan into the rear of the display, and plugged a Qwiic cable into the top of the Swan and the bottom of the Notecarrier B.
To power my project, I needed to use two LiPo batteries. While it is possible to transfer some power over qwiic, the Notecard sometimes needs more amperage than qwiic can provide when connecting to cell networks, but I found that two LiPos, even of the small variety, was plenty to power the project.
Before writing any firmware for this project, I needed to collect some images to paint to the display. The e-Ink FeatherWing supports bitmaps, which can either be stored in memory or loaded from a MicroSD card. I gathered up a handful of images, cropped them all to 250x122 (the size of the display) and then followed this super-helpful guide from Phillip Burgess to convert my images to bitmaps using Photoshop. The guide also includes instructions for ImageMagick if that's more your style.
Once I converted all my images, I dragged them onto a FAT-formatted MicroSD card and popped the card into the FeatherWing. If you're following along and want to use my images, you can grab them from the GitHub repo.
Setting up the Cloud BackendOne of the great things about the Notecard is it knows how to send data to a cloud backend, Notehub, 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.
After you create the project, make sure to copy your new project’s ProductUID, which you can get from the dashboard view, as you’ll need that identifier to connect your Notecard to the right Notehub project.
Now let's take a look at one of the most powerful capabilities of the Notehub and Notecard: environment variables.
Working with Environment VariablesThe Notecard is a bi-directional data-pump. It can send data from your devices to the cloud, and it can receive requests, data, and state from the cloud as well. For state management, the Notecard and Notehub provide a feature called environment variables.
Environment variables allow you to synchronize state and settings across fleets of devices of any size. They can be set at the project-, fleet-, and device-level, which means you can target a state change to a single device, all devices in a fleet, or all devices across all fleets in a project, and the Notecard handles scoping variables properly when synchronized with a given device.
For this project, I use environment variables to manage two pieces of state on each device. First is display_values
, which is a semicolon-delimited string consisting of text to display and/or bitmap images to load from the SD card to the e-Ink display. For example: "Foo!;myimg.bmp" will load the text "Foo!" on the screen first, and then attempt to load an image named myimg.bmp
from the SD card and display it on the screen.
If this variable has a single value (either text or an image name), that will be loaded. Otherwise, the values are loaded into an array and the host will display one item at a time.
The loading of text and images is managed by the second environment variable, display_interval_sec
, which is the number of seconds to display text or an image on the screen before rotating to the next item in the list. This value is ignored if the display_values
variable contains a single item.
Environment variables can be set either using the Notehub UI or API. Here's an example of using the UI to add a device-specific override of the display_values
variable on a single display. When synched, this device will display the text "Clean Out the Fridge!" while other devices in the fleet rotate through the text and images set at the fleet-level.
Now let's take a look at how the firmware brings all of this together. The full source needed to replicate this project is available on GitHub, but I'll show off the most important parts here. I used Arduino and PlatformIO for this app, with the Swan at the target. (And here are the instructions for getting all of that running on your device.)
Installing Libraries
Start by installing the following libraries, either in PlatformIO, or the Arduino IDE:
- Blues Wireless Notecard v1.3.13 or later;
- Adafruit Image Reader Library v2.81 or later;
- Adafruit GFX Library 1.11.13 or later;
The SD card library and other dependencies will come along for the ride.
You also will need the Adafruit EPD library, but at the time of this writing, it doesn't compile for STM32 boards. There's a PR up to fix the issue and if that's closed and merged when you read this, you can install the library and go on about your business. If not, you can use the version of the library included in the lib
directory of the GitHub project source.
Configure the Notecard
The next step is to configure the Notecard. The Notecarrier B 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 Digital Signage project via its ProductUID (com.blues.nf4
). In the GitHub source, you can find my full set of Notecard configuration requests in the notecard_config.h
file.
#define DEMO_MODE
Notecard notecard;
// in setup()
Wire.begin();
notecard.begin();
J *req = notecard.newRequest("hub.set");
if (req != NULL) {
JAddStringToObject(req, "product", PRODUCT_UID);
#ifdef DEMO_MODE
JAddStringToObject(req, "mode", "continuous");
JAddBoolToObject(req, "sync", true);
#else
// Tune for a long-running app that doesn't need to make
// immediate display updates.
JAddStringToObject(req, "mode", "periodic");
JAddBoolToObject(req, "align", true);
JAddNumberToObject(req, "inbound", 60); // Once an hour
// Voltage-variable outbound settings based on battery life
JAddStringToObject(req, "voutbound", "usb:60;high:1440;normal:1440;low:10080;0");
#endif
notecard.sendRequest(req);
}
The DEMO_MODE
declaration is a handy way to configure my Notecard for faster updates and a continuous connection during testing. When I'm ready to deploy, I can remove it and configure each device to operate in more of a low-power mode to maximize battery life.
Configuring the Display
Next, I need to configure the display and SD card reader library. If you're using a different e-Ink display, you'll need to tweak the display
constructor, and if you're using a different MCU, you'll want to check the pin mapping for your host.
#define EPD_DC 10
#define EPD_CS 9
#define EPD_BUSY -1
#define SRAM_CS 6
#define EPD_RESET -1
#define SD_CS 5
// 2.13" Tricolor EPD with IL0373 chipset
ThinkInk_213_Tricolor_RW display(EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY);
bool usingSD = false;
SdFat SD;
Adafruit_ImageReader_EPD reader(SD);
// in setup()
display.begin(THINKINK_TRICOLOR);
if(!SD.begin(SD_CS, SD_SCK_MHZ(10))) {
Serial.println("SD begin() failed");
} else {
usingSD = true;
}
Enumerating Images on the SD Card
For this project, I opted to use images on an SD card as opposed to loading those over the air, which is also possible with the Notecard and Notehub. For my deployment, however, I decided that I'd load up images on a set of cards, and deploy them to the displays using a sneaker-net approach.
So that I know which images are available on a device, I added a function that enumerates the bitmaps on a connected SD card, puts them in an array, and sends those to the Notecard and Notehub on startup.
void enumerateSDFiles() {
File dir;
File file;
if (!dir.open("/")){
serialDebugOut.println("dir.open failed");
}
J* req = notecard.newRequest("note.add");
if (req != NULL) {
JAddBoolToObject(req, "sync", true);
JAddStringToObject(req, "file", "image_files.qo");
J *body = JCreateObject();
if (body != NULL)
{
JAddStringToObject(body, "message","images detected on this display. Use the image environment variable to display an image to the screen.");
J* files = JAddArrayToObject(body, "images");
if (files != NULL) {
while (file.openNext(&dir, O_RDONLY)) {
char fileName[255];
file.getName(fileName, 255);
if (fileName[0] != '.' && !file.isDir()) {
JAddItemToArray(files, JCreateString(fileName));
}
file.close();
}
JAddItemToObject(req, "body", body);
notecard.sendRequest(req);
}
}
}
if (dir.getError()) {
serialDebugOut.println("file enumeration error.");
}
}
On Notehub, that note looks like this, and I can use that information to determine images available for display using the display_values
environment variable.
Checking for andFetching Environment Variable Updates
To check for Environment variable updates, I can either poll the Notecard, or use the ATTN pin to wake my host when updates are received. I opted for polling for this app, but the ATTN pin approach is covered here.
To poll the Notecard I make an env.modified
request and compare the returned time (which is the UNIX epoch timestamp of the last local variable update) to a locally-saved time.
bool pollEnvVars() {
if (millis() < nextPollMs) {
return false;
}
nextPollMs = millis() + (ENV_POLL_SECS * 1000);
J *rsp = notecard.requestAndResponse(notecard.newRequest("env.modified"));
if (rsp == NULL) {
return false;
}
uint32_t modifiedTime = JGetInt(rsp, "time");
notecard.deleteResponse(rsp);
if (lastModifiedTime == modifiedTime) {
return false;
}
lastModifiedTime = modifiedTime;
return true;
}
If a value has changed, I'll next use an env.get
request to retrieve the interval and values variables and save those to my local application state.
void fetchEnvironmentVariables(applicationState& state) {
applicationState vars = state;
J *req = notecard.newRequest("env.get");
J *names = JAddArrayToObject(req, "names");
JAddItemToArray(names, JCreateString("display_interval_sec"));
JAddItemToArray(names, JCreateString("display_values"));
J *rsp = notecard.requestAndResponse(req);
if (rsp != NULL) {
if (notecard.responseError(rsp)) {
notecard.deleteResponse(rsp);
return;
}
// Get the note's body
J *body = JGetObject(rsp, "body");
if (body != NULL) {
int displayInterval = atoi(JGetString(body, "display_interval_sec"));
if (displayInterval != vars.displayIntervalSec)
{
vars.displayIntervalSec = displayInterval;
vars.variablesUpdated = true;
displayUpdateInterval = vars.displayIntervalSec;
}
char *displayValues = JGetString(body, "display_values");
if (strcmp(vars.displayValues.c_str(), displayValues) != 0) {
vars.displayValues = String(displayValues);
vars.variablesUpdated = true;
vars.displayUpdated = false;
vars.currentDisplayObjectIndex = 0;
J *valueList = JCreateObject();
if (valueList != NULL) {
J* listItems = JAddArrayToObject(valueList, "list_items");
if (listItems != NULL) {
char* value = strtok(displayValues, ";");
if (value == NULL) {
JAddItemToArray(listItems, JCreateString(displayValues));
} else {
while (value != NULL) {
JAddItemToArray(listItems, JCreateString(value));
value = strtok(NULL, ";");
}
}
}
vars.displayObject = valueList;
}
}
state = vars;
}
}
notecard.deleteResponse(rsp);
}
Displaying Text
Now for the fun part: displaying text and images. Displaying text on an e-Ink display is straightforward, but I added some logic to my app that could accept a phrase or sentence and format it for the screen, both by decreasing the font size for larger text and to split the text across lines and center it (ish) on the display.
void displayText(const String& val) {
display.clearBuffer();
uint8_t textSize = 4;
uint8_t cursorY = 30;
int textLength = val.length();
char text[textLength+1];
val.toCharArray(text, textLength+1);
// Change the text size and cursor location based on the length
// of the string.
if (textLength > 16 && textLength <= 32) {
textSize = 3;
cursorY = 20;
} else if (textLength > 32 && textLength <= 64) {
textSize = 2;
cursorY = 10;
} else if (textLength > 64) {
textSize = 1;
cursorY = 5;
}
display.setCursor(0, cursorY);
display.setTextSize(textSize);
display.setTextColor(EPD_RED);
// Split the string by word and use the number of words to align and format
// the text so it looks centered on the display.
char* segment = strtok(text, " ");
while (segment != NULL) {
size_t segmentLen = strlen(segment);
// Determine the number of leading spaces we should have on the line
// in order to reasonably center the text
uint8_t padSpaces = floor((44 / textSize - segmentLen) / 2.00);
for (size_t i = 0; i < padSpaces; i++)
{
display.print(" ");
}
display.println(segment);
segment = strtok(NULL, " ");
}
display.display();
}
Displaying Images
For displaying images, I added some logic to send a Note to the Notecard if the image is not found on the SD card or the dimensions are wrong. Beyond that, the complexities of loading the image from the SD card and displaying it to the screen are handled by the Adafruit_ImageReader_EPD
library.
void displayImage(String fileName) {
Adafruit_Image_EPD img;
int32_t width = 0, height = 0;
ImageReturnCode ret;
const char *file = fileName.c_str();
if (!usingSD) {
return;
}
ret = reader.bmpDimensions(file, &width, &height);
if(ret == IMAGE_SUCCESS) {
if (width != 250 && height != 122)
{
J *body = JCreateObject();
if (body != NULL)
{
JAddStringToObject(body, "message", "image dimensions are incorrect.");
JAddStringToObject(body, "file", file);
JAddStringToObject(body, "app", "nf4");
sendNotifyNote(body);
}
}
reader.drawBMP((char *)file, display, 0, 0);
display.display();
} else {
J *body = JCreateObject();
if (body != NULL)
{
JAddStringToObject(body, "message", "image not found.");
JAddStringToObject(body, "file", file);
JAddStringToObject(body, "app", "nf4");
sendNotifyNote(body);
}
}
}
Rotating Content on the Display
Finally, I added a function to handle rotating text and images on the display when the display interval elapses. The collection of items to display and the current index are saved in my application state and I check the next item for a .bmp
extension to determine whether to display an image or text.
void rotateContent() {
// Rotate items on the display based on interval
J *itemsToDisplay = state.displayObject->child;
if (itemsToDisplay != NULL) {
int size = JGetArraySize(itemsToDisplay);
// If there's only one item, don't rotate
if (size == 1 && state.displayUpdated) {
return;
}
// Loop back around to the top of the list
if (state.currentDisplayObjectIndex == size) {
state.currentDisplayObjectIndex = 0;
}
J *item = JGetArrayItem(itemsToDisplay, state.currentDisplayObjectIndex);
// If the string contains a ".bmp" it's an image to display.
// Otherwise, assume it's text.
if (strstr(item->valuestring, ".bmp") != NULL) {
displayImage(item->valuestring);
} else if (strcmp(item->valuestring, "") != 0) {
displayText(item->valuestring);
} else {
// Clear the Display
display.clearBuffer();
display.display();
}
state.displayUpdated = true;
state.currentDisplayObjectIndex++;
}
}
Updating environment variables to display text and imagesOnce the app is up and running, I can update either a single display or a fleet of displays with the environment variables above, using the Notehub UI or the Notehub API.
The GitHub repo contains two shell scripts that you can download and run from a terminal to update the display interval or text and images on the screen. The scripts use the Notehub API, so you'll need follow the instructions here to get an authentication token.
You'll also need your Notehub project's UID, which you can find in the Settings
screen. For setting fleet variables, you'll need the Fleet UID, which you can find on the Settings
tab for your fleet. For device variables, you need the Device UID, which you can find on the device screen.
./update-device.sh -p app:1234 -d dev:5678 -t <your token> -i 45 -v 'Hello Notecard!;banner-ad.bmp;bluesio.bmp;Happy Wednesday;notecard_8bit.bmp;bw_logo.bmp;icon.bmp;logo.bmp;notecard.bmp'
The update-device.sh
and update-fleet.sh
scripts process the provided arguments and build up a cURL request against the Notehub environment-variables
api endpoint.
curl --request PUT \
--url "https://api.notefile.net/v1/projects/$product/devices/$device/environment_variables" \
--header "Content-Type: application/json" \
--header "X-SESSION-TOKEN: $token" \
--data "{\"environment_variables\":$env_vars}"
Finally, when the Notecard receives environment variable updates from Notehub, it delivers those to the Swan to update the display interval and/or text and images on the screen. Here's an example of my three devices in action.
Wrapping UpIf you're looking to deploy a low-power display solution using e-Ink displays, while also managing those displays from the cloud, the combination of the Swan and Notecard is worth checking out. To try this out yourself you’ll want to start with this project’s README on GitHub. From there, you’ll can pick up a few Notecards and Notecarrier Bs, Swans, and e-Ink displays. Then flash the project’s firmware and start managing your fleet of displays with cellular!
If you run into any issues feel free to reach out on the Blues Wireless forum for help.
Comments