Intermediate

Celebrate YouTube Milestones with Badgeware Blinky

Learn how to use Badgeware functions to add custom graphics and milestone animations to our YouTube subscriber app.

25 September 2026 · Tutorial · about 1 hour

Following on from part one of this series, we're delving deeper into advanced Badgeware features with Blinky. We'll start by formatting the layout of the app that we created in part one. Then, we will re-write the app to feature custom animations when we hit key YouTube milestones (1,000, 100,000, and 1,000,000 subscribers) and other personal goals and targets.

What You'll Need

Centring Images And Text

For the first part of this guide we'll be using the code from the previous tutorial as our base. If you are following on from that guide, then the code should already be on your Blinky.

The goal of this first section is to simply centre the YouTube logo and subscriber value on the screen. If you don't want flashy animations and text, then this is for you.

LesCrew Member
  1. Connect Blinky to your PC and press RESET twice to enter USB drive mode. Your PC will treat Blinky just like a typical USB flash drive. Image of Blinky 2350, the device is connected via USB to a PC.

  2. Open your operating system's file manager and navigate to a drive called BLINKY. Screenshot of Windows 11 file manager open at the BLINKY drive.

  3. In your preferred text editor, open the __init__.py file inside \apps\youtube_subs.

  4. Scroll down to def draw_subscribers(count):.

  5. Underneath value = format_count(count) add a new line to measure the width and height of the subscriber number text.

        w, h = screen.measure_text(value)
    
  6. Create a variable x to store the exact centre of the display. By deducting the width of the text from the width of the screen, then using floor division (dividing one number by another, rounding the result down to the nearest whole integer) we halve the values.

        x = (screen.width - w) // 2
    
  7. Look for screen.blit and screen.text and change their values to match these. The x value denotes the vertical centre line of the display.

        screen.blit(sprite, x,0)
        screen.text(value,x,9)
    
  8. Save the code and press RESET on Blinky to restart in badge mode.

  9. Scroll to the app, you should see the YouTube logo bounce on the screen. The YouTube logo, rendered in pixels on Blinky 2350.

  10. Press B to start the app. The case lights will flash until a solid Wi-Fi connection is made. Once connected, an API call is made and after a few seconds we will see the details on Blinky's display. The final app, showing 890 subscribers and the YouTube logo in the centre of the screen.

Complete Code Listing: Centred Image And Text

import wifi
import secrets
import time
import fetch

wifi.disconnect()
while not wifi.connect():
    badge.caselights(1)
    time.sleep(0.1)
    badge.caselights(0)
    time.sleep(0.1)

print("Connected! IP address:", wifi.ip())
badge.caselights(1)
API_URL = (
    "https://www.googleapis.com/youtube/v3/channels"
    "?part=statistics&id={0}&key={1}"
).format(secrets.CHANNEL_ID, secrets.API_KEY)

status = fetch.url(API_URL, every=3600)
last_subscribers = None


def format_count(n):
    if n >= 1_000_000:
        return "{:.1f}M".format(n / 1_000_000)
    if n >= 1_000:
        return "{:.1f}K".format(n / 1_000)
    return str(n)


def draw_subscribers(count):
    value = format_count(count)
    w, h = screen.measure_text(value)
    x = (screen.width - w) // 2
    screen.pen = color.black
    badge.clear()
    screen.pen = color.white
    sprite = image.load("assets/yt.png")
    screen.blit(sprite, x,0)
    screen.text(value,x,9)
    badge.update()


def get_youtube_stats(data):
    global last_subscribers
    try:
        stats = data["items"][0]["statistics"]
        subscribers = int(stats["subscriberCount"])
        print("Subscribers:", subscribers)

        if subscribers != last_subscribers:
            draw_subscribers(subscribers)
            last_subscribers = subscribers
    except (KeyError, IndexError):
        print("Unexpected response:", data)

while True:
    if status:
        get_youtube_stats(status.json())

Advanced Subscriber Goals - Animations

This section completely rewrites the original code, while packing lots of features covering the major YouTube subscriber milestones.

The features are

  • Custom boot animation for Wi-Fi connection.
  • Animations for 1000, 100000, and 1,000,000 subscribers.
  • Scrolling text.
  • YouTube logo created using vector primitives and polygons. No PNG image!

We're using primitives, vector shapes and polygons that we can dynamically transform to create animations. For example the Wi-Fi connection "spinner" is a simple triangle that is spun 30 degrees each loop. Do that 12 times and you can spin the triangle 360 degrees to show the ongoing Wi-Fi connection. We can't do that with a PNG file. The Badgeware API makes working with primitives very easy to do. We have a full section in the Badgeware documentation that shows more example of working with vectors. In this version of the project we also show the exact number of subscribers, which can become a huge number if you're a popular channel. By using Badgeware API's scrolling text feature, coupled with controlling where the text can be displayed, we create scrolling text inside a vector version of the YouTube logo.

Project Foundations

We'll be reusing the project files from the first version. So we will have a youtube_subs app folder, with __init__.py and assets inside of it.

  1. Connect Blinky to your PC and press RESET twice to enter USB drive mode. Your PC will treat Blinky just like a typical USB flash drive. Image of Blinky 2350, the device is connected via USB to a PC.

  2. Open your operating system's file manager and navigate to a drive called BLINKY. Screenshot of Windows 11 file manager open at the BLINKY drive.

  3. In your preferred text editor, open the __init__.py file inside \apps\youtube_subs.

  4. Backup the original __init__.py to a safe place and then delete the contents of __init__.py and save the file. There is a very small amount of code that is reused from the original version, but for clarity we'll start from a blank slate. The backup is optional, but you may want to keep the old code for future reference.

  5. Import a series of modules (pre-written Python code libraries that provide extra functionality).

    1. wifi: Badgeware specific helper function to use Blinky's onboard Wi-Fi chip to connect to a network and the Internet.
    2. secrets: Where our Wi-Fi SSID, password, and YouTube API key, and Channel ID are stored.
    3. time: Controls the pace at which the code runs.
    4. fetch: Badgeware specific helper function that we use to download data from the YouTube API.
    5. random: Introduces pseudo-randomness to our code. Primarily used to randomly place stars on the display.
    import wifi
    import secrets
    import time
    import fetch
    import random
    
  6. Set the display to use anti-aliasing for smoother and crisper vectors.

    screen.antialias = X4
    
  7. Create an object mona into which we load MonaSans-Medium.af a font that comes pre-installed on Badgeware. We need this font as we shall be dynamically controlling the size of text later.

    mona = font.load("/system/assets/fonts/MonaSans-Medium.af")
    
  8. Set the screen font to use mona.

    screen.font = mona
    
  9. Create an object unit_triangle that is placed in the top left (0,0) of the display, a radius of one pixel and three sides. This creates a vector triangle replicating the play button of the YouTube logo.

    unit_triangle = shape.regular_polygon(0, 0, 1, 3)
    
  10. Ensure that the Wi-Fi is disconnected. This is strictly an optional step, but we found it prudent to ensure that any lingering Wi-Fi connections were dropped, and fresh connections made before moving onward.

    wifi.disconnect()
    
  11. Using a while not loop, check that we are not connected to Wi-Fi. In essence this loop will try to connect to Wi-Fi. Each time it isn't connected, it will animate a triangle unit_triangle to spin while blinking Blinky's case lights to show that it is doing something.

    while not wifi.connect():
        for i in range(12):
            unit_triangle.transform = mat3().translate(screen.width//2, screen.height//2).scale(4).rotate(i*30)
            badge.caselights(1)
            time.sleep(0.1)
            screen.shape(unit_triangle)
            badge.update()
            badge.caselights(0)
            time.sleep(0.1)
    
  12. Print the connection details to the Python Shell, and then keep the case lights permanently on. The print to the Python Shell is a debug step used to confirm Blinky's IP address. The case lights staying on is there to visually identify that we have a Wi-Fi connection.

    print("Connected! IP address:", wifi.ip())
    badge.caselights(1)
    
  13. Build the URL that will be used to access the YouTube API. The URL will use our API key and Channel ID, which are stored inside secrets.py.

    API_URL = (
        "https://www.googleapis.com/youtube/v3/channels"
        "?part=statistics&id={0}&key={1}"
    ).format(secrets.CHANNEL_ID, secrets.API_KEY)
    
  14. Create an object status to store the returned data from the YouTube API every one hour (3,600 seconds). Using fetch and the freshly created API_URL we grab the data and store it in the object.

    status = fetch.url(API_URL, every=3600)
    
  15. Create an object, last_subscribers and store a None value. This prevents the app from crashing out if the API has a "blip" and doesn't provide any data.

    last_subscribers = None
    
  16. Create an object subscriber_window to handle the position and size of a rectangle inside the display, then create scroll which is used to identify when to scroll text on the screen.

    subscriber_window = screen.window(16, 6, 15, 10)
    scroll = None
    
  17. Use a function setup_scroller_and_animations which takes the number of subscribers as an argument. This function does the majority of the work in this project.

    def setup_scroller_and_animations(count):
    
  18. Create a global variable scroll that can be used inside and outside of the function. Then, create an object called value which converts the subscriber count into a string. Doing this, we can then pass that variable to scroll to scroll the subscriber count in a window, inside the YouTube logo. But more on that later!

        global scroll
        value = str(count)
        scroll = text.scroll(value, gap=10, speed=15, target=subscriber_window)
    
  19. Create two variables to store the output of screen width x and the screen height y halved using floor division. Then set the font to mona. Floor division is where we divide one number by another and then round the result down to the nearest whole integer.

        x = screen.width//2
        y = screen.height//2
        screen.font = mona
    
  20. Using a conditional test, check if the value of count is equal to or more than 1000 and less than or equal to 1,500. Because we poll every hour, we could miss the exact moment that we cross the threshold. By adding a buffer we can still trigger the animation. Change the 1,500 value to cater for how quickly you could pass the threshold. Bear in mind that a larger buffer means the animation will replay on every hourly poll (and every reboot) for as long as count stays inside the range, possibly for weeks. A smaller buffer reduces that repetition, but increases the chance of missing the milestone altogether, so tweak the buffer to strike whichever balance suits you. If you'd rather the animation only ever plays once, you could instead store the last milestone celebrated (e.g. in a file) and check against that before triggering.

        if count >= 1_000 and count <= 1500:
    
  21. If the condition has been met, a for loop will iterate 100 times, in reverse. The range counts down from 100 to 1, stopping before 0, decreasing by 1 each time.

            for i in range(100,0,-1):
    
  22. Update the value of s so that it contains the current value from the for loop iteration.

                s = i
    
  23. Create an object star which will be in the centre of the screen and shrink in size as the for loop counts down.

                star = shape.star(x, y, 5, s / 2, s)
    
  24. Draw the shape to the screen and then update so that the shape can be seen.

                screen.shape(star)
                badge.update()
    
  25. Wait for 0.01 seconds and then change the pen colour to black and then clear the screen using that pen colour.

                time.sleep(0.01)
                screen.pen = color.black
                screen.clear()
                badge.update()
    
  26. Using a for loop that iterates 50 times, set the pen colour to white, then write ONE THOUSAND !! inside a rectangle that starts in the top left of the display (0,0) and draw a 40 pixel box. The text is set to 8 pixels in height. Update the screen so that the text is displayed. Animated GIF showing a shrinking star, followed by flashing text and then the scrolling subscriber count.

            for i in range(50):
                screen.pen = color.white
                screen.text("ONE THOUSAND !!", rect(0, 0, 40, 40),8)
                badge.update()
    
  27. Add a short delay, then set the pen colour to grey and repeat the same text, in the same position. This produces a flashing text effect, very similar to late 20th century movie theatre signage.

                time.sleep(0.1)
                screen.pen = color.grey
                screen.text("ONE THOUSAND !!", rect(0, 0, 40, 40),8)
                badge.update()
                time.sleep(0.1)
    
  28. Outside of the for loop, set the pen colour to black and clear the screen. This resets the screen after flashing the text, ready for the subs counter to appear.

            screen.pen = color.black
            screen.clear()
            badge.update()
    
  29. Create a conditional section for 100,000 subscribers. This has a similar buffer to 1,000 but we increase the buffer to 1,000 subscribers. This time there is one central star, which grows until it fully encompasses the screen. Then ONE HUNDRED THOUSAND is printed to the screen using the same flashing text effect. Animated GIF showing a growing star, followed by flashing text and then the scrolling subscriber count.

        elif count >= 100_000 and count <= 101_000:
            for i in range(100):
                s = i
                star = shape.star(x, y, 5, s / 2, s)
                screen.shape(star)
                badge.update()
                time.sleep(0.01)
                screen.pen = color.black
                screen.clear()
                badge.update()
            for i in range(50):
                screen.pen = color.white
                screen.text("ONE HUNDRED THOUSAND", rect(0, -3, 40, 40),8)
                badge.update()
                time.sleep(0.1)
                screen.pen = color.grey
                screen.text("ONE HUNDRED THOUSAND", rect(0, -3, 40, 40),8)
                badge.update()
                time.sleep(0.1)
            screen.pen = color.black
            screen.clear()
            badge.update() 
    
  30. This next section is an example of using a custom value to trigger an action. We've set this to our current number of subscribers, 890, and it simply reuses the flashing text effect. But this could be any value of your choosing. In the complete code listing below, this is commented out. To activate, remove the # from the beginning of each line and tweak the trigger.

        elif count >= 890 and count <= 1234: #THIS IS A CUSTOM TARGET FOR YOUR CHANNEL, UNCOMMENT AND CHANGE ACCORDINGLY
            for i in range(50):
                screen.pen = color.white
                screen.text("CUSTOM TRIGGER", rect(0, -3, 40, 40),8)
                badge.update()
                time.sleep(0.1)
                screen.pen = color.grey
                screen.text("CUSTOM TRIGGER", rect(0, -3, 40, 40),8)
                badge.update()
                time.sleep(0.1)
            screen.pen = color.black
            screen.clear()
            badge.update()
    
  31. Add a section for one million subscribers. This will draw stars at random positions on the screen, before displaying flashing text. Again we have added a buffer, this time 1,000 subscribers, so that the event is not missed. Animated GIF showing random stars appearing across the screen, followed by flashing text and then the scrolling subscriber count.

        elif count >= 1_000_000 and count <= 1_001_000:
            screen.pen = color.white
            for i in range(50):
                screenx = random.randint(0, screen.width)
                screeny = random.randint(0, screen.height)
                s = i
                star = shape.star(screenx, screeny, 5, s / 2, s)
                screen.shape(star)
                badge.update()
                time.sleep(0.1)
            screen.pen = color.black
            screen.clear()
            badge.update()
            for i in range(100):
                screen.pen = color.white
                screen.text("ONE MILLION !!", rect(0, 0, 40, 40),8)
                badge.update()
                time.sleep(0.1)
                screen.pen = color.grey
                screen.text("ONE MILLION !!", rect(0, 0, 40, 40),8)
                badge.update()
                time.sleep(0.1)
            screen.pen = color.black
            screen.clear()
            badge.update()
    
  32. Create a function, draw_frame which handles drawing elements to the display and calls the code to run scroll(). Draw frame can clear the screen to black, draw a grey outer rounded rectangle which when used with a smaller, black rounded rectangle, and a triangle rotated 30 degrees, can be used to make a facsimile of the YouTube logo using no PNG images.

    def draw_frame():
        screen.pen = color.black
        badge.clear()
    
        screen.pen = color.grey
        outer = shape.rounded_rectangle(6, 2, 28, 18, 2)
        screen.shape(outer)
    
        screen.pen = color.black
        inner = shape.rounded_rectangle(7, 3, 26, 16, 2)
        screen.shape(inner)
    
        screen.pen = color.grey
        unit_triangle.transform = mat3().translate(10, 11).scale(4).rotate(30)
        screen.shape(unit_triangle)
    
        screen.pen = color.white
        if scroll:
            scroll()
    
  33. This function get_youtube_stats handles extracting the subscriber stats from what the YouTube API returns and updates the subscribers variable. The function also handles if there is an error in the returned data. You'll notice that there is a hard coded value for subscribers which has been commented out. Python will ignore this, but if we need to test an animation ahead of hitting the milestone, we can hard code the value and test. Just remember to comment # the code to deactivate and trigger using the returned API value.

    def get_youtube_stats(data):
        global last_subscribers
        try:
            stats = data["items"][0]["statistics"]
            subscribers = int(stats["subscriberCount"])
            #HARD CODE SUBSCRIBER NUMBER FOR TESTING!!!
            #subscribers = 1000000
            if subscribers != last_subscribers:
                setup_scroller_and_animations(subscribers)
                last_subscribers = subscribers
        except (KeyError, IndexError):
            print("Unexpected response:", data)
    
  34. This while True loop will check the status of the fetched data, and calls the draw_frame function to write the data to the display.

    while True:
        if status:
            get_youtube_stats(status.json())
        draw_frame()
        badge.update()
    
  35. Save the code and press RESET on Blinky to restart in badge mode.

  36. Scroll to the app, you should see the YouTube logo bounce on the screen. The YouTube logo, rendered in pixels on Blinky 2350.

  37. Press B to start the app. The case lights will flash and the triangle progress indicator will spin until a solid Wi-Fi connection is made. Once connected, an API call is made and after a few seconds we will see either an animation or our current subscriber details on Blinky's display.

Your YouTube subscriber badge is now complete and you can now wear it proudly in a video or hide it on the set for eagle-eyed viewers to find.

Complete Code Listing: Advanced Subscriber Goals

import wifi
import secrets
import time
import fetch
import random


screen.antialias = X4
mona = font.load("/system/assets/fonts/MonaSans-Medium.af")
screen.font = mona
unit_triangle = shape.regular_polygon(0, 0, 1, 3)

wifi.disconnect()
while not wifi.connect():
    for i in range(12):
        unit_triangle.transform = mat3().translate(screen.width//2, screen.height//2).scale(4).rotate(i*30)
        badge.caselights(1)
        time.sleep(0.1)
        screen.shape(unit_triangle)
        badge.update()
        badge.caselights(0)
        time.sleep(0.1)

print("Connected! IP address:", wifi.ip())
badge.caselights(1)
API_URL = (
    "https://www.googleapis.com/youtube/v3/channels"
    "?part=statistics&id={0}&key={1}"
).format(secrets.CHANNEL_ID, secrets.API_KEY)

status = fetch.url(API_URL, every=3600)
last_subscribers = None

subscriber_window = screen.window(16, 6, 15, 10)
scroll = None

def setup_scroller_and_animations(count):
    global scroll
    value = str(count)
    scroll = text.scroll(value, gap=10, speed=15, target=subscriber_window)
    x = screen.width//2
    y = screen.height//2
    screen.font = mona
    if count >= 1_000 and count <= 1500:
        for i in range(100,0,-1):
            s = i
            star = shape.star(x, y, 5, s / 2, s)
            screen.shape(star)
            badge.update()
            time.sleep(0.01)
            screen.pen = color.black
            screen.clear()
            badge.update()
        for i in range(50):
            screen.pen = color.white
            screen.text("ONE THOUSAND !!", rect(0, 0, 40, 40),8)
            badge.update()
            time.sleep(0.1)
            screen.pen = color.grey
            screen.text("ONE THOUSAND !!", rect(0, 0, 40, 40),8)
            badge.update()
            time.sleep(0.1)
        screen.pen = color.black
        screen.clear()
        badge.update()
    elif count >= 100_000 and count <= 101_000:
        for i in range(100):
            s = i
            star = shape.star(x, y, 5, s / 2, s)
            screen.shape(star)
            badge.update()
            time.sleep(0.01)
            screen.pen = color.black
            screen.clear()
            badge.update()
        for i in range(50):
            screen.pen = color.white
            screen.text("ONE HUNDRED THOUSAND", rect(0, -3, 40, 40),8)
            badge.update()
            time.sleep(0.1)
            screen.pen = color.grey
            screen.text("ONE HUNDRED THOUSAND", rect(0, -3, 40, 40),8)
            badge.update()
            time.sleep(0.1)
        screen.pen = color.black
        screen.clear()
        badge.update()        
    #elif count >= 890 and count <= 1234: #THIS IS A CUSTOM TARGET FOR YOUR CHANNEL, UNCOMMENT AND CHANGE ACCORDINGLY
    #    for i in range(50):
    #        screen.pen = color.white
    #        screen.text("CUSTOM TRIGGER", rect(0, -3, 40, 40),8)
    #        badge.update()
    #        time.sleep(0.1)
    #        screen.pen = color.grey
    #        screen.text("CUSTOM TRIGGER", rect(0, -3, 40, 40),8)
    #        badge.update()
    #        time.sleep(0.1)
    #    screen.pen = color.black
    #    screen.clear()
    #    badge.update() 
    elif count >= 1_000_000 and count <= 1_001_000:
        screen.pen = color.white
        for i in range(50):
            screenx = random.randint(0, screen.width)
            screeny = random.randint(0, screen.height)
            s = i
            star = shape.star(screenx, screeny, 5, s / 2, s)
            screen.shape(star)
            badge.update()
            time.sleep(0.1)
        screen.pen = color.black
        screen.clear()
        badge.update()
        for i in range(100):
            screen.pen = color.white
            screen.text("ONE MILLION !!", rect(0, 0, 40, 40),8)
            badge.update()
            time.sleep(0.1)
            screen.pen = color.grey
            screen.text("ONE MILLION !!", rect(0, 0, 40, 40),8)
            badge.update()
            time.sleep(0.1)
        screen.pen = color.black
        screen.clear()
        badge.update()


def draw_frame():
    screen.pen = color.black
    badge.clear()

    screen.pen = color.grey
    outer = shape.rounded_rectangle(6, 2, 28, 18, 2)
    screen.shape(outer)

    screen.pen = color.black
    inner = shape.rounded_rectangle(7, 3, 26, 16, 2)
    screen.shape(inner)

    screen.pen = color.grey
    unit_triangle.transform = mat3().translate(10, 11).scale(4).rotate(30)
    screen.shape(unit_triangle)

    screen.pen = color.white
    if scroll:
        scroll()


def get_youtube_stats(data):
    global last_subscribers
    try:
        stats = data["items"][0]["statistics"]
        subscribers = int(stats["subscriberCount"])
        #HARD CODE SUBSCRIBER NUMBER FOR TESTING!!!
        #subscribers = 1000000
        if subscribers != last_subscribers:
            setup_scroller_and_animations(subscribers)
            last_subscribers = subscribers
    except (KeyError, IndexError):
        print("Unexpected response:", data)

while True:
    if status:
        get_youtube_stats(status.json())
    draw_frame()
    badge.update()

What Have We Learnt?

  • How to draw vector images on Badgeware, using shapes and polygons instead of PNGs.
  • How to transform vector shapes with mat3, to translate, scale, and rotate them for animations.
  • How to use screen.window and scrolling text to display long or changing values.
  • How to use different fonts on Badgeware.
  • How to create animations with Badgeware.