For GitHub Universe 2025 we have partnered with our friends at Pimoroni to create the Tufty Edition of our hackable conference badge. This is a custom version of the Pimoroni Tufty 2350 badge, with a custom PCB, added IR sensors and pre-loaded with some fun apps. The source code for the custom apps is available here.
These apps are designed to be run on the base MonaOS MicroPython firmware pre-installed on your badge.
Your badge this year is based on Pimoroni's upcoming "Tufty" Badgeware product. It's the fourth, most ambitious, and best, digital badge that we've developed for GitHub Universe so far!
On the board we've packed in a bunch of great features:
- RP2350 Dual-core ARM Cortex-M33 @ 200MHz
- 512kB SRAM
- 16MB QSPI XiP flash
- 320x240 full colour IPS display (currently pixel doubled to 160x120)
- 2.4GHz WiFi and Bluetooth 5
- 1000mAh chargeable battery (up to 8 hours runtime)
- Qw/ST and SWD ports for accessories and debugging
- 4 GPIO pins + power through-hole solder pads for additional hardware hacking
- IR receiver and transmitter for beacon hunting and remote control
- Five front facing buttons
- Battery voltage monitoring and charge status
- USB-C port for charging and programming
- RESET / BOOTSEL buttons
- 4-zone LED backlight
- Durable polycarbonate case with lanyard fixings
We hope you really love tinkering with it during, and after, the event. Tag the Pimoroni team on BlueSky and show them what you're up to!
This documentation is an early draft and both it and the API are subject to change! We're putting together a new build with more features and some performance enhancements which we'll share soon!
The board is pre-loaded with a special build of MicroPython that includes drivers for all of the built-in hardware.
We've also included a small suite of example applications to demonstrate how different features of the badge work. Feel free to poke around in the code and experiment!
For an overview of how to put the badge into different modes checkout https://badger.github.io/get-started/.
The structure for apps is as follows. They live in the /system/apps directory.
/system
/apps
/my_application
icon.png
__init__.py
/assets
...
Each application has a minimal structure:
icon.pngcontains the icon for your app, it should be a 24x24 PNG image.__init__.pythe entry point for your application, this is whereupdate()will be implemented.assets/a directory to contain any assets your app uses, this is automatically added to the system path when your app launches.
Your app should implement an update() function within __init__.py which will automatically be called every frame to give it a chance to update its state and render new output to the screen.
You'll have to update the menu app on your device to see your app, the version of the pre-flashed firmware only supports six icons - have fun expanding it!
An app is launched by main.py, which handles the intro cinematic, menu and launching your app. It'll call your init() and update() methods, and call on_exit() when you press the HOME button to leave your app.
# example __init__.py for an application
# select a font to use
screen.font = PixelFont.load("nope.ppf")
# called once when your app launches
def init():
pass
# called every frame, do all your stuff here!
def update():
# clear the framebuffer
screen.brush = brushes.color(20, 40, 60)
screen.clear()
# calculate and draw an animated sine wave
y = (math.sin(io.ticks / 100) * 20) + 80
screen.brush = brushes.color(0, 255, 0)
for x in range(160):
screen.draw(shapes.rectangle(x, y, 1, 1))
# write a message
screen.brush = brushes.color(255, 255, 255)
screen.text("hello badge!", 10, 10)
# called before returning to the menu to allow you to save state
def on_exit():
passThe easiest way to edit the code on the device is to put it into mass storage mode:
- Connect your badge up to your computer via a USB-C cable
- Press the RESET button twice
- The USB Disk Mode screen will appear
- The badge should appear as a disk named "BADGER" on your computer
In this mode you can see the contents of the FAT32 /system/ mount. This is where all application code should live.
When your badge arrives, it will be pre-loaded with a factory default Micropython image that will have a custom image of Micropython with our apps pre-installed and a 'MonaOS' app launcher.
If you want to reset your badge back to factory conditions, you will need to flash the latest Micropython firmware that we have. You can find the latest firmware image in the Releases
- Download the latest
.uf2firmware file from the releases page. - Connect your badge to your computer via USB.
- On the back of the badge, press and hold the
Homebutton. While holding downHome, press and release theResetbutton. Then release theHomebutton. - Your badge should then appear as a USB drive named
RP2350. - Drag and drop the
.uf2file onto theRP2350drive. - The badge will automatically reboot and run the new firmware.
The badge has a writeable LittleFS partition located at / which is intended for applications to store state information and cache any data they may need to hold on to across resets.
You can use normal Python style file access from your code:
with open("/storage/myfile.txt", "w") as out:
out.write("this is some text i want to keep\n")The io module also exposes helpful information about the state of the buttons on your badge. Click here for full documentation of the io module.
Each button is represented by a constant (for example, io.BUTTON_A). The API lets you check whether a button has been pressed, released, held, or if its state has changed during the current frame.
The following example draws a small white circle in the centre of the screen and allows the user to move it using the buttons:
import math
from badgeware import screen, shapes, brushes, io
# start the cursor in the middle of the screen
x, y = 80, 60
# clamp a value to within an upper and lower bound
def clamp(value, lower, upper):
return math.max(lower, math.min(upper, value))
# called automatically every frame
def update():
global x, y
# clear the screen
screen.brush = brushes.color(20, 40, 60)
screen.draw(shapes.rectangle(0, 0, 160, 120))
# move cursor based on button states
if io.BUTTON_A in io.held: x -= 1 # left
if io.BUTTON_C in io.held: x += 1 # right
if io.BUTTON_UP in io.held: y -= 1 # up
if io.BUTTON_DOWN in io.held: y += 1 # down
# clamp position to screen bounds
x = clamp(x, 0, 160)
y = clamp(y, 0, 120)
# draw the cursor
screen.brush = brushes.color(255, 255, 255)
screen.draw(shapes.circle(x, y, 5))The framebuffer is a 160 × 120 true colour Image named screen. Click here for full documentation of the Image class.
Your application can draw to the screen during the update() function. After your code finishes executing, the badge automatically pixel-doubles the framebuffer to fit the display.
The screen supports drawing vector shapes, blitting other images, and rendering text using pixel fonts.
The shapes module provides ways to create primitive shapes which can then be drawn onto images. Click here for full documentation of the shapes module.
Currently supported shapes are rectangles, circles, arcs, pies, lines, rounded rectangles, regular polygons, and squircles.
Shapes are drawn using the currently assigned brush. Click here for full documentation of the brushes module.
# example of drawing a circle in the center of the screen
from badgeware import screen, brushes, shapes
# enable antialiasing for lush smooth edges
screen.antialias = Image.X4
def update():
# clear the background
screen.brush = brushes.color(20, 40, 60)
screen.clear()
# draw the circle
screen.brush = brushes.color(0, 255, 0)
screen.draw(shapes.circle(80, 60, 20))Shapes can also be given a transformation matrix to adjust their scale, rotation, and skew - this is very useful for creating smooth animations. Click here for full documentation of the Matrix class.
# example of animating a rotating rectangle
from badgeware import screen, brushes, shapes
# enable antialiasing for lush smooth edges
screen.antialias = Image.X4
def update():
# clear the background
screen.brush = brushes.color(20, 40, 60)
screen.clear()
# transform and draw the rectangle
screen.brush = brushes.color(0, 255, 0)
rectangle = shapes.rectangle(-1, -1, 2, 2)
rectangle.transform = Matrix().translate(80, 60).scale(20, 20).rotate(io.ticks / 100)
screen.draw(rectangle)The Image class can load images from files on the filesystem which can then be blitted onto the screen. Click here for full documentation of the Image class.
# example of loading a sprite and blitting it to the screen
from badgeware import screen, brushes, Image
from lib import SpriteSheet
# load an image as a sprite sheet specifying the number of rows and columns
mona = SpriteSheet(f"assets/mona-sprites/mona-default.png", 7, 1)
def update():
# clear the background
screen.brush = brushes.color(20, 40, 60)
screen.clear()
# blit the sprite at 0, 0 in the spritesheet to the screen
screen.blit(mona.sprite(0, 0), 10, 10)
# scale blit the sprite at 3, 0 in the spritesheet to the screen
screen.scale_blit(mona.sprite(0, 0), 50, 50, 30, 30)The PixelFont class provides functions for loading pixel fonts, which can then be used to render text onto images. Click here for full documentation of the PixelFont class.
There are thirty licensed pixel fonts included.
# example of drawing text to the screen
from badgeware import screen, PixelFont
screen.font = PixelFont.load("nope.ppf")
def update():
screen.brush = brushes.color(20, 40, 60)
screen.clear()
message = "this font rocks"
screen.brush(255, 255, 255)
screen.text(message, 10, 10)You can use the existing MicroPython functionality for wireless networking and bluetooth functionality.