This project works together with the Adafruit Bluefruit LE Connect app for iOS and Android to control a scrolling message across the 18x5 RGB LED matrix.
When first run, the glasses will scroll the message “RUN BLUEFRUIT CONNECT APP”. Self explanatory. When you run this app on your phone or tablet, you’ll see the EyeLights device as “LED Glasses Driver nRF52840.” Tap the corresponding “Connect” button.
Once connected, the app will show this “MODULES” screen. The items of interest here are UART and CONTROLLER.
UART provides a text field into which you can type a message, up to a maximum of 50 characters. Press the “Send” button to update the glasses.
Tip: the exclamation point character (“!”) is off-limits…that has special meaning to the software. Any other ASCII characters (letters, numbers, punctuation) are fair game. Upper or lower case doesn’t matter…because the matrix is small and requires a chunky font, everything will be converted to upper case.
Or, from the “Controller” screen, click “Color Picker.”
Here you can select a color from the wheel and a brightness level with the slider. Tapping “Send selected color” will change the color of the scrolling message.
The above two settings — message and color — are the only options that have any bearing on this project. Although the app can issue other data like game pad buttons or a compass heading, we didn’t want to go overboard and make the code too complex to follow. Consider it a starting point for your own ideas.
The text looks rough when testing right in front of you…but from a few feet away the image blends together and is more legible. Try it with a mirror!
If you’d prefer a pre-compiled binary: download this .UF2 file. Connect the EyeLights driver board to your computer with a USB cable, set the power switch “on,” double-tap the reset button and a small flash drive named GLASSESBOOT appears. Then drag the .UF2 file to GLASSESBOOT and wait several seconds while it copies.
Here is the source code for the two files required. Put them into the same Arduino sketch folder.
EyeLights_Bluetooth_Scroller.ino
// SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/*
BLUETOOTH SCROLLING MESSAGE for Adafruit EyeLights (LED Glasses + Driver).
Use BLUEFRUIT CONNECT app on iOS or Android to connect to LED glasses.
Use the app's UART input to enter a new message.
Use the app's Color Picker (under "Controller") to change text color.
This is based on the glassesdemo-3-smooth example from the
Adafruit_IS31FL3741 library, with Bluetooth additions on top. If this
code all seems a bit too much, you can start with that example (or the two
that precede it) to gain an understanding of the LED glasses basics, then
return here to see what the extra Bluetooth layers do.
*/
#include <Adafruit_IS31FL3741.h> // For LED driver
#include <bluefruit.h> // For Bluetooth communication
#include <EyeLightsCanvasFont.h> // Smooth scrolly font for glasses
// These items are over in the packetParser.cpp tab:
extern uint8_t packetbuffer[];
extern uint8_t readPacket(BLEUart *ble, uint16_t timeout);
extern int8_t packetType(uint8_t *buf, uint8_t len);
extern float parsefloat(uint8_t *buffer);
extern void printHex(const uint8_t * data, const uint32_t numBytes);
// GLOBAL VARIABLES -------
// 'Buffered' glasses for buttery animation,
// 'true' to allocate a drawing canvas for smooth graphics:
Adafruit_EyeLights_buffered glasses(true);
GFXcanvas16 *canvas; // Pointer to glasses' canvas object
// Because 'canvas' is a pointer, always use -> when calling
// drawing functions there. 'glasses' is an object in itself,
// so . is used when calling its functions.
char message[51] = "Run Bluefruit Connect app"; // Scrolling message
int16_t text_x; // Message position on canvas
int16_t text_min; // Leftmost position before restarting scroll
BLEUart bleuart; // Bluetooth low energy UART
int8_t last_packet_type = 99; // Last BLE packet type, init to nonsense value
// ONE-TIME SETUP ---------
void setup() { // Runs once at program start...
Serial.begin(115200);
//while(!Serial);
// Configure and start the BLE UART service
Bluefruit.begin();
Bluefruit.setTxPower(4);
bleuart.begin();
startAdv(); // Set up and start advertising
if (!glasses.begin()) err("IS3741 not found", 2);
canvas = glasses.getCanvas();
if (!canvas) err("Can't allocate canvas", 5);
// Configure glasses for full brightness and enable output
glasses.setLEDscaling(0xFF);
glasses.setGlobalCurrent(0xFF);
glasses.enable(true);
// Set up for scrolling text, initialize color and position
canvas->setFont(&EyeLightsCanvasFont);
canvas->setTextWrap(false); // Allow text to extend off edges
canvas->setTextColor(glasses.color565(0x303030)); // Dim white to start
reposition_text(); // Sets up initial position & scroll limit
}
// Crude error handler, prints message to Serial console, flashes LED
void err(char *str, uint8_t hz) {
Serial.println(str);
pinMode(LED_BUILTIN, OUTPUT);
for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1);
}
// Set up, start BLE advertising
void startAdv(void) {
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
// Include the BLE UART (AKA 'NUS') 128-bit UUID
Bluefruit.Advertising.addService(bleuart);
// Secondary Scan Response packet (optional)
// Since there is no room for 'Name' in Advertising packet
Bluefruit.ScanResponse.addName();
// Start Advertising
// - Enable auto advertising if disconnected
// - Interval: fast mode = 20 ms, slow mode = 152.5 ms
// - Timeout for fast mode is 30 seconds
// - Start(timeout) with timeout = 0 will advertise forever (until connected)
//
// For recommended advertising interval
// https://developer.apple.com/library/content/qa/qa1931/_index.html
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
// MAIN LOOP --------------
void loop() { // Repeat forever...
// The packet read timeout (9 ms here) also determines the text
// scrolling speed -- if no data is received over BLE in that time,
// the function exits and returns here with len=0.
uint8_t len = readPacket(&bleuart, 9);
if (len) {
int8_t type = packetType(packetbuffer, len);
// The Bluefruit Connect app can return a variety of data from
// a phone's sensors. To keep this example relatively simple,
// we'll only look at color and text, but here's where others
// would go if we were to extend this. See Bluefruit library
// examples for the packet data formats. packetParser.cpp
// has a couple functions not used in this code but that may be
// helpful in interpreting these other packet types.
switch(type) {
case 0: // Accelerometer
Serial.println("Accel");
break;
case 1: // Gyro:
Serial.println("Gyro");
break;
case 2: // Magnetometer
Serial.println("Mag");
break;
case 3: // Quaternion
Serial.println("Quat");
break;
case 4: // Button
Serial.println("Button");
break;
case 5: // Color
Serial.println("Color");
// packetbuffer[2] through [4] contain R, G, B byte values.
// Because the drawing canvas uses lower-precision '565' color,
// and because glasses.scale() applies gamma correction and may
// quantize the dimmest colors to 0, set a brightness floor here
// so text isn't invisible.
for (uint8_t i=2; i<=4; i++) {
if (packetbuffer[i] < 0x20) packetbuffer[i] = 0x20;
}
canvas->setTextColor(glasses.color565(glasses.Color(
packetbuffer[2], packetbuffer[3], packetbuffer[4])));
break;
case 6: // Location
Serial.println("Location");
break;
default: // -1
// Packet is not one of the Bluefruit Connect types. Most programs
// will ignore/reject it as not valud, but in this case we accept
// it as a freeform string for the scrolling message.
if (last_packet_type != -1) {
// If prior data was a packet, this is a new freeform string,
// initialize the message string with it...
strncpy(message, (char *)packetbuffer, 20);
} else {
// If prior data was also a freeform string, concatenate this onto
// the message (up to the max message length). BLE packets can only
// be so large, so long strings are broken into multiple packets.
uint8_t message_len = strlen(message);
uint8_t max_append = sizeof message - 1 - message_len;
strncpy(&message[message_len], (char *)packetbuffer, max_append);
len = message_len + max_append;
}
message[len] = 0; // End of string NUL char
Serial.println(message);
reposition_text(); // Reset text off right edge of canvas
}
last_packet_type = type; // Save packet type for next pass
} else {
last_packet_type = 99; // BLE read timeout, reset last type to nonsense
}
canvas->fillScreen(0); // Clear the whole drawing canvas
// Update text to new position, and draw on canvas
if (--text_x < text_min) { // If text scrolls off left edge,
text_x = canvas->width(); // reset position off right edge
}
canvas->setCursor(text_x, canvas->height());
canvas->print(message);
glasses.scale(); // 1:3 downsample canvas to LED matrix
glasses.show(); // MUST call show() to update matrix
}
// When new message text is assigned, call this to reset its position
// off the right edge and calculate column where scrolling resets.
void reposition_text() {
uint16_t w, h, ignore;
canvas->getTextBounds(message, 0, 0, (int16_t *)&ignore, (int16_t *)&ignore, &w, &ignore);
text_x = canvas->width();
text_min = -w; // Off left edge this many pixels
}
packetParser.cpp
// SPDX-FileCopyrightText: 2021 Phillip Burgess for Adafruit Industries
//
// SPDX-License-Identifier: MIT
#include <bluefruit.h>
// packetbuffer holds inbound data
#define READ_BUFSIZE 20
uint8_t packetbuffer[READ_BUFSIZE + 1]; // +1 is for NUL string terminator
/**************************************************************************/
/*!
@brief Casts the four bytes at the specified address to a float.
@param ptr Pointer into packet buffer.
@returns Floating-point value.
*/
/**************************************************************************/
float parsefloat(uint8_t *ptr) {
float f; // Make a suitably-aligned float variable,
memcpy(&f, ptr, 4); // because in-buffer instance might be misaligned!
return f; // (You can't always safely parse in-place)
}
/**************************************************************************/
/*!
@brief Prints a series of bytes in 0xNN hexadecimal notation.
@param buf Pointer to array of byte data.
@param len Data length in bytes.
*/
/**************************************************************************/
void printHex(const uint8_t *buf, const uint32_t len) {
for (uint32_t i=0; i < len; i++) {
Serial.print(F("0x"));
if (buf[i] <= 0xF) Serial.write('0'); // Zero-pad small values
Serial.print(buf[i], HEX);
if (i < (len - 1)) Serial.write(' '); // Space between bytes
}
Serial.println();
}
static const struct { // Special payloads from Bluefruit Connect app...
char id; // Packet type identifier
uint8_t len; // Size of complete, well-formed packet of this type
} _app_packet[] = {
{'A', 15}, // Accelerometer
{'G', 15}, // Gyro
{'M', 15}, // Magnetometer
{'Q', 19}, // Quaterion
{'B', 5}, // Button
{'C', 6}, // Color
{'L', 15}, // Location
};
#define NUM_PACKET_TYPES (sizeof _app_packet / sizeof _app_packet[0])
#define SHORTEST_PACKET_LEN 5 // Button, for now
/**************************************************************************/
/*!
@brief Given packet data, identify if it's one of the known
Bluefruit Connect app packet types.
@param buf Pointer to packet data.
@param len Size of packet in bytes.
@returns Packet type index (0 to NUM_PACKET_TYPES-1) if recognized,
-1 if unrecognized.
*/
/**************************************************************************/
int8_t packetType(uint8_t *buf, uint8_t len) {
if ((len >= SHORTEST_PACKET_LEN) && (buf[0] == '!')) {
for (int8_t type=0; type<NUM_PACKET_TYPES; type++) {
if ((buf[1] == _app_packet[type].id) &&
(len == _app_packet[type].len)) {
return type;
}
}
}
return -1; // Length too short for a packet, or not a recognized type
}
/**************************************************************************/
/*!
@brief Wait for incoming data and determine if it's one of the
special Bluefruit Connect app packet types.
@param ble Pointer to BLE UART object.
timeout Character read timeout in milliseconds.
@returns Length of data, or 0 if checksum is invalid for the type of
packet detected.
@note Packet buffer is not cleared. Calling function is expected
to check return value before deciding whether to act on the
data.
*/
/**************************************************************************/
uint8_t readPacket(BLEUart *ble, uint16_t timeout) {
int8_t type = -1; // App packet type, -1 if unknown or freeform string
uint8_t len = 0, xsum = 255; // Packet length and ~checksum so far
uint32_t now, start_time = millis();
do {
now = millis();
if (ble->available()) {
char c = ble->read();
if (c == '!') { // '!' resets buffer to start
len = 0;
xsum = 255;
}
packetbuffer[len++] = c;
// Stop when buffer's full or packet type recognized
if ((len >= READ_BUFSIZE) ||
((type = packetType(packetbuffer, len)) >= 0)) break;
start_time = now; // Reset timeout on char received
xsum -= c; // Not last char, do checksum math
type = -1; // Reset packet type finder
}
} while((now - start_time) < timeout);
// If packet type recognized, verify checksum (else freeform string)
if ((type >= 0) && (xsum != packetbuffer[len-1])) { // Last byte = checksum
Serial.print("Packet checksum mismatch: ");
printHex(packetbuffer, len);
return 0;
}
packetbuffer[len] = 0; // Terminate packet string
return len; // Checksum is valid for packet, or it's a freeform string
}
Page last edited October 01, 2025
Text editor powered by tinymce.