Get ready for your next conference with the PyBadge Conference Badge. With this simple project, not only can you show off some of your basic details for people to contact you, but you can also control the NeoPixels at the bottom to draw people's attention to your badge.

This project displays a simple name tag that is drawn using code, so it is easy to customize without the need to do any graphical editing. Additionally, since CircuitPython is capable of displaying unicode, fonts are very customizable.

Parts

Adafruit PyBadge for MakeCode Arcade, CircuitPython, or Arduino
What's the size of a credit card and can run CircuitPython, MakeCode Arcade or Arduino? That's right, its the Adafruit PyBadge! We wanted to see how much we...
$34.95
In Stock
Adafruit PyBadge LC - MakeCode Arcade, CircuitPython, or Arduino
What's the size of a credit card and can run CircuitPython, MakeCode Arcade or Arduino even when you're on a budget? That's right, it's the Adafruit...
$24.95
In Stock

Accessories

Be sure to grab a lanyard so you can easily show off your badge.

Adafruit Circuit Playground Lanyard
We've got our Circuit Playground friends on lunchboxes, 
$1.95
In Stock

This project doesn't use the speaker, but they're inexpensive and very easy to add in case you decide to get creative and add sound. 

Mini Oval Speaker - 8 Ohm 1 Watt
Hear the good news! This wee speaker is a great addition to any audio project where you need 8 ohm impedance and 1W or less of power. We particularly like...
Out of Stock

Also, be sure to pick up a couple of batteries in case they get low on power so you can just swap them out. The PyBadge comes with built-in charging circuitry, so they're very easy to charge.

Lithium Ion Polymer Battery Ideal For Feathers - 3.7V 400mAh
Lithium ion polymer (also known as 'lipo' or 'lipoly') batteries are thin, light and powerful. The output ranges from 4.2V when completely charged to 3.7V. This battery...
$6.95
In Stock

To get set up, we will need CircuitPython, a few libraries and the Conference Badge Source Code and fonts downloaded from Github.

Required CircuitPython Libraries

First, make sure you are running the latest version of Adafruit CircuitPython for your board.

Next, you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle.  Our introduction guide has a great page on how to install the library bundle for both express and non-express boards.

Since the fonts take up a good deal of space, Adafruit recommends that you manually install only the necessary libraries from the bundle:

  • neopixel
  • adafruit_bitmap_font
  • adafruit_display_shapes
  • adafruit_display_text

Before continuing make sure your board's lib folder or root filesystem have the neopixel, adafruit_bitmap_font, adafruit_display_shapes, and adafruit_display_text files and folders copied over.

Conference Badge Source Code

Next, we'll continue by downloading the Source Code from Github.

The fonts go in a subdirectory of your PyBadge's main CIRCUITPY flash drive named /fonts

Here is the CircuitPython code in its entirety. If you click on Download Project Zip, it will download the fonts used in this tutorial also. 

"""
This is a Conference Badge type Name Tag that is intended to be displayed on
the PyBadge. Feel free to customize it to your heart's content.
"""

import time
from math import sqrt, cos, sin, radians
import board
from micropython import const
import displayio
import digitalio
import neopixel
from gamepadshift import GamePadShift
from adafruit_display_shapes.rect import Rect
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font

# Button Constants
BUTTON_LEFT = const(128)
BUTTON_UP = const(64)
BUTTON_DOWN = const(32)
BUTTON_RIGHT = const(16)
BUTTON_SEL = const(8)
BUTTON_START = const(4)
BUTTON_A = const(2)
BUTTON_B = const(1)

# Customizations
HELLO_STRING = "HELLO"
MY_NAME_STRING = "MY NAME IS"
NAME_STRING = "Blinka"
NAME_FONTNAME = "/fonts/Noto-18.bdf"
NEOPIXEL_COUNT = 5
BACKGROUND_COLOR = 0xFF0000
FOREGROUND_COLOR = 0xFFFFFF
BACKGROUND_TEXT_COLOR = 0xFFFFFF
FOREGROUND_TEXT_COLOR = 0x000000

# Default Values
brightness = 0.2
direction = 1
speed = 1

# Define the NeoPixel and Game Pad Objects
neopixels = neopixel.NeoPixel(board.NEOPIXEL, NEOPIXEL_COUNT, brightness=brightness,
                              auto_write=False, pixel_order=neopixel.GRB)

pad = GamePadShift(digitalio.DigitalInOut(board.BUTTON_CLOCK),
                   digitalio.DigitalInOut(board.BUTTON_OUT),
                   digitalio.DigitalInOut(board.BUTTON_LATCH))

# Make the Display Background
splash = displayio.Group(max_size=20)
board.DISPLAY.show(splash)

color_bitmap = displayio.Bitmap(160, 128, 1)
color_palette = displayio.Palette(1)
color_palette[0] = BACKGROUND_COLOR

bg_sprite = displayio.TileGrid(color_bitmap,
                               pixel_shader=color_palette,
                               x=0, y=0)
splash.append(bg_sprite)

# Draw a Foreground Rectangle where the name goes
rect = Rect(0, 50, 160, 70, fill=FOREGROUND_COLOR)
splash.append(rect)

# Load the Hello font
large_font_name = "/fonts/Verdana-Bold-18.bdf"
large_font = bitmap_font.load_font(large_font_name)
large_font.load_glyphs(HELLO_STRING.encode('utf-8'))

# Load the "My Name Is" font
small_font_name = "/fonts/Arial-12.bdf"
small_font = bitmap_font.load_font(small_font_name)
small_font.load_glyphs(MY_NAME_STRING.encode('utf-8'))

# Load the Name font
name_font_name = NAME_FONTNAME
name_font = bitmap_font.load_font(name_font_name)
name_font.load_glyphs(NAME_STRING.encode('utf-8'))

# Setup and Center the Hello Label
hello_label = Label(large_font, text=HELLO_STRING)
(x, y, w, h) = hello_label.bounding_box
hello_label.x = (80 - w // 2)
hello_label.y = 15
hello_label.color = BACKGROUND_TEXT_COLOR
splash.append(hello_label)

# Setup and Center the "My Name Is" Label
mni_label = Label(small_font, text=MY_NAME_STRING)
(x, y, w, h) = mni_label.bounding_box
mni_label.x = (80 - w // 2)
mni_label.y = 35
mni_label.color = BACKGROUND_TEXT_COLOR
splash.append(mni_label)

# Setup and Center the Name Label
name_label = Label(name_font, text=NAME_STRING, line_spacing=0.75)
(x, y, w, h) = name_label.bounding_box
name_label.x = (80 - w // 2)
name_label.y = 85
name_label.color = FOREGROUND_TEXT_COLOR
splash.append(name_label)

# Remap the calculated rotation to 0 - 255
def remap(vector):
    return int(((255 * vector + 85) * 0.75) + 0.5)

# Calculate the Hue rotation starting with Red as 0 degrees
def rotate(degrees):
    cosA = cos(radians(degrees))
    sinA = sin(radians(degrees))
    red = cosA + (1.0 - cosA) / 3.0
    green = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
    blue = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
    return (remap(red), remap(green), remap(blue))

palette = []
pixels = []

# Generate a rainbow palette
for degree in range(0, 360):
    color = rotate(degree)
    palette.append(color[0] << 16 | color[1] << 8 | color[2])

# Create the Pattern
for x in range(0, NEOPIXEL_COUNT):
    pixels.append(x * 360 // NEOPIXEL_COUNT)

# Main Loop
current_buttons = pad.get_pressed()
last_read = 0
while True:
    for color in range(0, 360, speed):
        for index in range(0, NEOPIXEL_COUNT):
            palette_index = pixels[index] + color * direction
            if palette_index >= 360:
                palette_index -= 360
            elif palette_index < 0:
                palette_index += 360
            neopixels[index] = palette[palette_index]
        neopixels.show()
        neopixels.brightness = brightness
        # Reading buttons too fast returns 0
        if (last_read + 0.1) < time.monotonic():
            buttons = pad.get_pressed()
            last_read = time.monotonic()
        if current_buttons != buttons:
            # Respond to the buttons
            if (buttons & BUTTON_RIGHT) > 0:
                direction = -1
            elif (buttons & BUTTON_LEFT) > 0:
                direction = 1
            elif (buttons & BUTTON_UP) > 0 and speed < 10:
                speed += 1
            elif (buttons & BUTTON_DOWN) > 0 and speed > 1:
                speed -= 1
            elif (buttons & BUTTON_A) > 0 and brightness < 0.5:
                brightness += 0.025
            elif (buttons & BUTTON_B) > 0 and brightness > 0.025:
                brightness -= 0.025
            current_buttons = buttons

After copying everything over, your PyBadge should display a Nametag with Blinka on it and the neopixels should animate. In the next few sections, we'll go over the Fonts and how to make changes.

In order to provide as much language support in a single font, we looked at quite a few different ones. We decided to choose one of the Google Noto fonts. The specific font is the Noto Sans Display font due to its ability to support over 500 languages through its support of the Latin, Greek, and Cyrillic character sets. It is called a Noto Font because the goal of the font was to avoid needing to display the placeholder blocks that represent the missing characters. These placeholder blocks are referred to as "Tofu", so the name of the font is short for No Tofu.

If you would like to make your own custom font, be sure to check out our Custom Fonts for CircuitPython Displays guide. For the fonts in this guide, we used otf2bdf.

For the basic Badge, we have a simple Hello, My Name Is Blinka. You can change the name "Blinka" to whatever your name is. 

But what if your name has non-English characters? If your name has accents, that's easy since the Noto font has all of the accented characters. But let's say Blinka happened to be from Greece and had Greek characters. CircuitPython will handle this with no problems.

That's because CircuitPython handles unicode. In this case since Blinka isn't normally composed of Greek characters, we chose some equivalent Greek characters to demonstrate the badge's ability to display Greek. But what about Cyrillic characters like Russian? Yes, no problem.

Other Character Sets

Ok, so we know we can write out the name in many languages just with these three character sets. If you have a name that uses another character set such as Arabic, Thai, Korean, Japanese or Chinese, there are plenty more font sets available from Google. In fact they have a page where you can type in your language and it will show you the fonts they have available in that language.

Of course you aren't limited to these fonts. You may find other font sets are much smaller and fit into the memory space on the PyBadge better. Here's an example of a Japanese Font with only katakana characters.

CircuitPython is even capable of handling fonts that should display Right to Left, with a small caveat. You will need to reverse the string before displaying it. You can easily do this with the following line of code.

NAME_STRING = ''.join(reversed(NAME_STRING))

Afterwards, it should display similar to this name tag that displays Blinka in Hebrew.

Changing the Hello Message

What if you would like to change the Hello message on the Badge? Yes, you can easily do that. Let's say we have a French Blinka.

We can easily change the labels to say the equivalent message in French.

Now let's take a look at some other customizations we can do to the badge on the following pages.

The name tag can be customized in a number of ways. Let's take a look at the easiest option to change first.

Basic Customizations

HELLO_STRING = "HELLO"
MY_NAME_STRING = "MY NAME IS"
NAME_STRING = "Blinka"
NAME_FONTNAME = "/fonts/Noto-18.bdf"
NEOPIXEL_COUNT = 5
BACKGROUND_COLOR = 0xFF0000
FOREGROUND_COLOR = 0xFFFFFF
BACKGROUND_TEXT_COLOR = 0xFFFFFF
FOREGROUND_TEXT_COLOR = 0x000000

The most obvious customization is changing your name. Just edit NAME_STRING. You can even us a \n newline character in the middle of your string to break your information into two lines. The text will automatically center both horizontally and vertically. You can also change the message at the top by editing HELLO_STRING and MY_NAME_STRING.

The next items to change are the colors of the badge. Colors are Hexadecimal values in the format of RRGGBB. For instance, you can change the red BACKGROUND_COLOR, which is currently 0xFF0000, to anything you like as well as the white FOREGROUND_COLOR

For instance, if we set BACKGROUND_COLOR to 0x0000FF, which is Blue andFOREGROUND_COLOR to 0xFFFF00, which is yellow, the result would look something like this.

You can also easily set the BACKGROUND_TEXT_COLOR to change the text on top of the red background or the FOREGROUND_TEXT_COLOR to change the color of your name.

If you have a custom font that you would like to use, you can set it by changing the NAME_FONTNAME to correct path.

The last item here that you can change is the NEOPIXEL_COUNT. If you have one of the PyBadge LC models with only one NeoPixel, you will want to change this to 1.

Advanced Customizations

Additionally, you can change anything else you want in this name tag which includes:

  • The fonts of the other labels
  • Positions of all the labels
  • Individual colors of the labels
  • Adding graphics
  • Anything else you can think of

Since the badge has built-in NeoPixels, let's animate them.

Now let's take a look at the code that does the animation.

CircuitPython NeoPixel Code

brightness = 0.2
direction = 1
speed = 1

neopixels = neopixel.NeoPixel(board.NEOPIXEL, 5, brightness=brightness,
                              auto_write=False, pixel_order=neopixel.GRB)

# Remap the calculated rotation to 0 - 255
def remap(vector):
    return int(((255 * vector + 85) * 0.75) + 0.5)

# Calculate the Hue rotation starting with Red as 0 degrees
def rotate(degrees):
    cosA = cos(radians(degrees))
    sinA = sin(radians(degrees))
    red = cosA + (1.0 - cosA) / 3.0
    green = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
    blue = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
    return (remap(red), remap(green), remap(blue))

palette = []
pixels = []

# Generate a rainbow palette
for degree in range(0, 360):
    color = rotate(degree)
    palette.append(color[0] << 16 | color[1] << 8 | color[2])

# Create the Pattern
for x in range(0, NEOPIXEL_COUNT):
    pixels.append(x * 360 // NEOPIXEL_COUNT)

while True:
    for color in range(0, 360, speed):
        for index in range(0, NEOPIXEL_COUNT):
            palette_index = pixels[index] + color * direction
            if palette_index >= 360:
                palette_index -= 360
            elif palette_index < 0:
                palette_index += 360
            neopixels[index] = palette[palette_index]
        neopixels.show()
        neopixels.brightness = brightness

This can be broken down. Let's look at each section.

We will begin by including NEOPIXEL_COUNT, which was discussed in the previous section. Next, we set the default brightness, which is a float value between 0.0 and 1.0, the direction which will be either 1 or -1, and the speed which has to be a positive intor integer. Finally, we define the NeoPixel object.

NEOPIXEL_COUNT = 5

brightness = 0.2
direction = 1
speed = 1

neopixels = neopixel.NeoPixel(board.NEOPIXEL, NEOPIXEL_COUNT, brightness=brightness,
                              auto_write=False, pixel_order=neopixel.GRB)

In the next section we define a couple of functions that do the math for generating the rainbow palette. The rotate() function does most of the work and takes in a number between 0 and 360 degrees, with 0 degrees being red, and performs calculations to get the red, blue, and green component values. The values are then remapped with the remap() function to be between 0 and 255.

# Remap the calculated rotation to 0 - 255
def remap(vector):
    return int(((255 * vector + 85) * 0.75) + 0.5)

# Calculate the Hue rotation starting with Red as 0 degrees
def rotate(degrees):
    cosA = cos(radians(degrees))
    sinA = sin(radians(degrees))
    red = cosA + (1.0 - cosA) / 3.0
    green = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
    blue = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
    return (remap(red), remap(green), remap(blue))

In the next section, we pre-calculate the red, blue, and green components of each of the colors so that we don't need to do as much math during the animation sequence and add them into a palette array. For the pattern, we are really just trying to choose some numbers that are distributed evenly.

palette = []
pixels = []

# Generate a rainbow palette
for degree in range(0, 360):
    color = rotate(degree)
    palette.append(color[0] << 16 | color[1] << 8 | color[2])

# Create the Pattern
for x in range(0, NEOPIXEL_COUNT):
    pixels.append(x * 360 // NEOPIXEL_COUNT)

Finally, we get to the main loop and do a simple palette rotation. We do this by going through all of the degrees in the color wheel. Depending on the speed, we may skip a few steps to make it go faster. We simply need to get the index of the color we want in the palette that we had pre-calculated.

We can change the direction by making the index a negative number, which is then adjusted to be within the range of 0-360 and we have our index. We look up the color by providing the index to the palette and then assign the color to the neopixel.

We also set the brightness of the NeoPixels here in case we changed it.

while True:
    for color in range(0, 360, speed):
        for index in range(0, NEOPIXEL_COUNT):
            palette_index = pixels[index] + color * direction
            if palette_index >= 360:
                palette_index -= 360
            elif palette_index < 0:
                palette_index += 360
            neopixels[index] = palette[palette_index]
        neopixels.show()
        neopixels.brightness = brightness

To use the PyBadge buttons, we will need a few pieces of code. Here's how the buttons function:

  • Pushing Up make the lights cycle faster
  • Pushing Down makes the lights cycle slower
  • Pushing Right makes the lights appear to move to the right
  • Pushing Left makes the buttons appear to move to the left
  • Pushing the B Button makes the lights dimmer
  • Pushing the A Button makes the light brighter

CircuitPython Button Code

BUTTON_LEFT = const(128)
BUTTON_UP = const(64)
BUTTON_DOWN = const(32)
BUTTON_RIGHT = const(16)
BUTTON_SEL = const(8)
BUTTON_START = const(4)
BUTTON_A = const(2)
BUTTON_B = const(1)

pad = GamePadShift(digitalio.DigitalInOut(board.BUTTON_CLOCK),
                   digitalio.DigitalInOut(board.BUTTON_OUT),
                   digitalio.DigitalInOut(board.BUTTON_LATCH))

def check_buttons(buttons):
    global direction, speed, brightness
    if (buttons & BUTTON_RIGHT) > 0:
        direction = -1
    elif (buttons & BUTTON_LEFT) > 0:
        direction = 1
    elif (buttons & BUTTON_UP) > 0 and speed < 10:
        speed += 1
    elif (buttons & BUTTON_DOWN) > 0 and speed > 1:
        speed -= 1
    elif (buttons & BUTTON_A) > 0 and brightness < 0.5:
        brightness += 0.025
    elif (buttons & BUTTON_B) > 0 and brightness > 0.025:
        brightness -= 0.025

current_buttons = pad.get_pressed()
last_read = 0
while True:
    for color in range(0, 360, speed):
        if (last_read + 0.1) < time.monotonic():
            buttons = pad.get_pressed()
            last_read = time.monotonic()
        if current_buttons != buttons:
            check_buttons(buttons)
            current_buttons = buttons

Originally GamePadShift was designed for reading the button inputs for games, but we are slightly repurposing it for use in changing settings. This means we need to detect when a button is pressed and released. We do this by monitoring the state of all the buttons and respond only when there is a change. Let's look at the code a little closer.

BUTTON_LEFT = const(128)
BUTTON_UP = const(64)
BUTTON_DOWN = const(32)
BUTTON_RIGHT = const(16)
BUTTON_SEL = const(8)
BUTTON_START = const(4)
BUTTON_A = const(2)
BUTTON_B = const(1)

There are eight buttons on the PyBadge and each corresponds to a bit in the byte of data that is returned. In the first part, we define which button corresponds to the appropriate value. We do that by defining each of the buttons as bit masks. Bit masking works by defining which bits we want to look at and which we want to hide or "mask". For instance we have BUTTON_SEL defined as 8, which is the same as 0x00001000 in binary. This is because we want to only look at the fourth bit from the right and ignore all the others.

If we get back a value from GamePadShift that is something like 0x01001000 then we know that the BUTTON_SEL is being pressed at the same time as BUTTON_UP which has a bit mask value of 64 or 0x01000000.

In the next segment of code we setup the GamePadShift object and call it pad.

pad = GamePadShift(digitalio.DigitalInOut(board.BUTTON_CLOCK),
                   digitalio.DigitalInOut(board.BUTTON_OUT),
                   digitalio.DigitalInOut(board.BUTTON_LATCH))

We do that by passing the Clock, Latch, and Out pins of the built-in shift register to the module. This is an SN74HC165 Parallel-Load Shift Register, which works by first "latching" the current input values of the buttons being pressed. Then the Out pin is read and the data is shifted each time a Clock pulse is sent. This way we can get any combination of the eight inputs from the shift register. This is all handled in the background which makes it much easier to use.

Finally let's take a look at the button code in the main loop, which handles the button presses.

current_buttons = pad.get_pressed()
last_read = 0
while True:
    for color in range(0, 360, speed):
        # Reading buttons too fast returns 0
        if (last_read + 0.1) < time.monotonic():
            buttons = pad.get_pressed()
            last_read = time.monotonic()
        if current_buttons != buttons:
            # Respond to the buttons
            if (buttons & BUTTON_RIGHT) > 0:
                direction = -1
            elif (buttons & BUTTON_LEFT) > 0:
                direction = 1
            elif (buttons & BUTTON_UP) > 0 and speed < 10:
                speed += 1
            elif (buttons & BUTTON_DOWN) > 0 and speed > 1:
                speed -= 1
            elif (buttons & BUTTON_A) > 0 and brightness < 0.5:
                brightness += 0.025
            elif (buttons & BUTTON_B) > 0 and brightness > 0.025:
                brightness -= 0.025
            current_buttons = buttons

We start by grabbing the current value of the buttons and setting the time of the last read to 0. After that, we have a for loop to cycle through all of the different colors. I have omitted that code so that we can focus on the button code only.

We then compare the last time the buttons were read with the current time. time.monotonic() will return the time that has elapsed in seconds since the processor was powered up. We only want to read the buttons every 0.1 seconds. Since we don't need the buttons to be as responsive as it would need to be for a game, this works great for this purpose.

In the last bit, it just has a bunch of else statements that check the bit mask of the passed in button value as well as checking the limits. If you would like to add functionality to the Start and Select buttons, this would probably be the easiest place to add it.

This guide was first published on May 28, 2019. It was last updated on May 28, 2019.