This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

ESP32 platform firmware

This section of the documentation describes the ESP32 platform firmware used on the SHA2017 and many other ESP32 related badges. For the MCH2022 badge new, modernized firmware will be developed which will of course be made available for the earlier badges at a later date. For MCH2022 related documentation please check out the MCH2022 section of the documentation.

1 - Hatchery

The hatchery lives at and is a repository of apps for use on your badge.


Registration is simple, email can be whatever, for example, it is only used for password resets.

App model

Apps are folders with as a minimal requirement a file.

Hatchery will add an empty version of that file for you.


This is what should be run after import by the [[SHA2017Badge/Launcher|Launcher]].

This is what will be started on boot (if present) like so from app import on_boot

You can use this to run TSR apps (take a look at the flashlight app for an example of this..


Unless you upload or create such a file, Hatchery will generate one . .

This contains at-minimum the description of the app and weather or not it should be shown in the [[SHA2017Badge/Launcher|Launcher]].

Hatching eggs

Installation of apps on the badge is done with [[SHA2017Badge/woezel|woezel]] via REPL or with help of a graphical [[SHA2017Badge/Installer|Installer]] on the badge.


There’s an api available, used by [[SHA2017Badge/woezel|woezel]] and [[SHA2017Badge/Installer|Installer]]:

/eggs/get/[app]/json       - get json data for a the egg named [app]
/eggs/list/json            - a list of all eggs with name, slug, description, revision
/eggs/search/[words]/json  - json data for search query [words]
/eggs/categories/json      - json list of categories
/eggs/category/[cat]/json  - json data for category [cat]

Since the merger there are now baskets for different badges

/basket/[badge]/list/json           - a list of all eggs for specific [badge]
/basket/[badge]/search/json         - [badge] specific search for [words]
/basket/[badge]/category/[cat]/json - json data for category [cat] on [badge]

You can play around with this API here at:


Hatchery on Github

2 - ESP32: app development

This section describes the API of the BADGE.TEAM ESP32 platform firmware and serves as a reference for developing apps for our ESP32 based badges.

Getting started

The getting started section will help you get started with all this awesomeness..

API reference

Once your first “hello world” app is up-and-running you’re probably wondering “how do I do…”. The API reference gives you detailed, in-depth information about the things you can do with your badge.

Publishing your app

Now that your app is ready to be shared with the world the hatchery section will help you with publishing your app.

2.1 - Getting started

One of the aims of the BADGE.TEAM project is to ensure that as many people as possible can develop software for the platform. This ensures that badges and other hardware running our firmware are more likely to have a life beyond the event for which they were created.

The problem with many event badges has been that the learning curve for new developers is too steep and the acceptance process for new software too difficult. When detailed knowledge of a toolchain is required to write code and each addition must wait to be built into a fresh badge firmware update, most would-be developers go away and enjoy the event instead.

With an online app store we refer to as the hatchery and MicroPython apps we refer to as eggs, publishing new software for badges running our firmware is a much simpler process.

Not everybody is immediately familiar with a new platform though, so to help with your first badge egg we’ve created this tutorial. The aim is not to teach you Python but to introduce you to the structure of an extremely basic egg as well as get you going with the user interface. We’ll be following the time-honoured tradition of introducing you to badge programming with a “Hello world” egg.

Connecting to your badge

First make sure you’re able to connect to your badge. The exact driver needed for the USB to serial bridge on your badge differs. Make sure to follow the guide for your specific badge.

After you have installed the correct driver you can connect to your badge using a terminal emulation program.

For Windows we recommend either TeraTerm or Putty).

Connect to your badge at 115200 baud. After waking up your badge from sleep mode you should be presented with a menu. You can wake your badge up from sleep mode either by pressing or touching a button or by pressing the RESET button (if available).

After you’ve succesfully connected to your badge you can continue your quest by creating your first egg, click here!.

Which type of badge do you have?

The different badges do not all have exactly the same hardware, so there are some slight differences in the setup process.

Please click on the badge you have to go to the getting started guide for your badge.

CampZone 2020
Disobey 2020 (Secret!)
CampZone 2019
HackerHotel 2019
Disobey 2019

2.1.1 - Your first egg

In this tutorial you will create an app which displays “Hello, world!” on the display of your badge, while reacting to input from the buttons on your badge.

Executing code

After you connect to your badge (and wake it up) you will be greeted by the built in menu. Selecting the “Python shell” menu option and pressing RETURN to accept you will be greeted by a prompt.


On this shell you can type python commands like you would on a computer. For example you could enter print("Hello, world!") to have your badge echo that same text back to you.

Should you want to paste bigger chuncks of code at once then you can use the builtin “paste mode” to do so. You can access this mode by pressing CTRL+E on your keyboard. You can then press CTRL+D to execute the pasted code or press CTRL+C to cancel.

Pressing CTRL+D outside of paste mode will reboot your badge, returning you back to the menu. Pressing CTRL+C outside of paste mode will stop the currently running command or app and return to the shell.

The display

To display text, images, shapes or other graphics on the display of your badge you use the display API.

The following example code demonstrates how to display some text on the display of your badge. It consists of four commands.

First we import the display library, allowing us to use the functions of this library in our app.

Then we fill the display with white (0xFFFFFF) and draw on top using black (0x000000). These colors are in RGB24 format, which is also commonly used for web-development. If you never heard of colors in the #000000 format then you might want to look up a tutorial on web colors first.

Even after filling the screen with white and drawing some text the display hasn’t been updated, it will still be showing whatever it did before we started… To send the buffer we just manipulated to the display you use the flush command. This way of working allows you to assemble an image before updating the screen.

import display

# Fill the framebuffer with white

# Draw text at (0,0) in black using the 'PermanentMarker22' font
display.drawText(0,0,"Hello, world!", 0x000000, "PermanentMarker22")

# Flush the contents of the framebuffer to the display

Depening on your badge it might be wise to use a smaller font to test with, for example the 7x5 font.

import display
display.drawText(0,0,"Hello, world!", 0x000000, "7x5")


For working with the buttons on your badge you use the buttons library.

Each button can be attached to a function with the following structure: def myCallback(pressed):. The argument is True when the function was called because the button was pressed and False when the function was called because the buttton got released.

You can assign a function to each button separately using buttons.attach(<button>, <function>).

The following demonstration code shows how to react to a button:

import buttons

def myCallback(pressed):
	if pressed:
		print("Button callback: pressed!")
		print("Button callback: released!")

buttons.attach(buttons.BTN_A, myCallback)

Combining the two!

import display, buttons

def message(text):
	display.drawText(0,0, text, 0x000000, "7x5")

def myCallback(pressed):
	if pressed:

buttons.attach(buttons.BTN_A, myCallback)

message("Press the A button!")

If your badge does not have the A button then you can substitute that button with any other button. The Python prompt on your badge has tab completion. Just enter buttons.BTN_ and press TAB on your keyboard for a list of available buttons.

And further?

Documenting is hard and a very slow process for us hackers. Therefore we suggest you take a look at one of the many apps published in the Hatchery to gain some inspiration and to publish your own app.

2.2 - USB-serial connection

You can communicate with your badge when it is not sleeping.

You can use a terminal application such as picocom to start talking to your badge. Hit ‘?’ to open the text menu, which you can use to enter a micropython shell.

You can use tools like ampy and mpfshell to transfer files between your PC and the badge, and execute python code from there. Sometimes you need a couple attempts for a request to succeed.

2.3 - API reference

Welcome to the API reference for the BADGE.TEAM platform firmware.

This reference describes all officially supported APIs of our platform. We try to keep these APIs as stable as possible. There are many more (undocumented) APIs in the firmware, all of which may change at any time!

Our platform firmware uses MicroPython at it’s core. Most of the libraries and APIs from the upstream MicroPython project are available in the BADGE.TEAM firmware.

The MicroPython documentation describes the builtin libraries and functions.

Specifically, the MicroPython core in our firmware is based on the ESP32 port of MicroPython by Loboris. He changed some parts of MicroPython to suit the ESP32 better. The wiki of his project describes the changes he made.

We have made a lot of changes on top of the work done by Loboris. We’ve added some badge specific APIs, a brand new framebuffer system for displaying graphics and drivers for the hardware specific to the supported badges.

By doing this we aim to take the resource intensive parts of driving the hardware to the C level beneath Python. This allows for a much more speedy experience and a lot more possibilities and flexibility.

Things to keep in mind while looking up documentation

  • There is currently no API available for directly controlling the SPI bus(ses) of your badge from within Python.
  • I2C should be used with caution as the I2C bus on most badges is used for system peripherals as well.
  • The Neopixel (LED) driver differs greatly from the neopixel API in the Loboris port.
  • The Display driver differs greatly from the display API in the Loboris port.

If you want to help with firmware development please tell us! We’re always happy to accept PRs and improvements.

Should you have ideas, problems or observations but no means to act on them then you can always create an issue on Github.

BADGE.TEAM platform APIs

Library Function MCH 2022 SHA2017 Disobey 2019 HackerHotel 2019 CampZone 2019 CampZone 2020
display Control the display of your badge: create and display text and graphics
buttons Read button status and attach callback functions to button interactions
wifi Abstraction layer wrapping the network API for connection to WiFi networks
system Abstraction layer for starting apps and controlling badge behaviour and sleep mode
consts Reference containing constants describing your badge and it’s firmware
audio Easy to use wrapper around sndmixer for playing audio files
sndmixer Audio related functions in active development, may change at ANY time Partially
terminal Helper functions for presenting a user interface over the serial port or telnet
neopixel Control the addressable LEDs on your badge
mpu6050 MPU6050 accelerometer and gyroscope control
ugTTS A small library to generate and play back Text-to-Speech voice messages
espnow Mesh networking API utilizing the Espressif ESPNOW features of the ESP32
hid Send keyboard and mouse events over USB (only on supported boards)
midi Send MIDI messages over USB (only on supported boards)
keypad CampZone 2020 specific silicon keypad button event handler
touchpads Register callbacks that trigger when ESP32 touch pads are touched
samd Disobey 2019 specific hardware interface module
rgb Legacy display API for CampZone 2019 badges
keyboard Display a text entry form complete with on-screen-keyboard
umqtt MQTT client library
ssd1306 Direct SSD1306 display control (will be removed in a future release)
erc12864 Direct ERC12864 display control (will be removed in a future release)
eink Direct E-INK display control (will be removed in a future release)
rtc Legacy real-time-clock API (please use machine.RTC and utime instead) ✅  
_buttons Generic GPIO button handler API, usefull for adding extra buttons to GPIO headers ✅  
voltages API for reading various voltages, exact functionality differs per badge
esp32_ulp Collection of helper functions for using the Ultra Low Power co-processor

APIs that differ from their upstream counterparts

Other libraries and APIs

This section lists most of the other libraries that you can use in your apps.

Library Function Documentation
math Mathematical functions MicroPython
cmath Mathematical functions for complex numbers MicroPython
ubinascii Utilities for working with binary data (Hex-string, base64 and CRC32 calculation MicroPython
ucollections Collection and container types MicroPython
uerrno System error code reference MicroPython
uhashlib SHA1 and SHA256 hashing algorithms MicroPython
uheapq Heap queue algorithm MicroPython
uio Input/output streams MicroPython
ujson JSON encoding and decoding MicroPython
uos Basic “operating system” services MicroPython
ure Simple regular expressions MicroPython
uselect Wait for events on a set of streams MicroPython
usocket Sockets (TCP, UDP) MicroPython
ussl SSL/TLS module MicroPython
ustruct Pack and unpack primitive data types MicroPython
utime Time related functions MicroPython
uzlib Zlib decompression MicroPython
_thread Multithreading support MicroPython
gc Control the garbage collector MicroPython
sys System specific functions MicroPython
machine Functions related to the hardware (Note: different from upstream version) [BADGE.TEAM]](machine)
micropython Access and control MicroPython internals MicroPython
network Network configuration (Please use the wifi library instead when possible) MicroPython
esp ESP32 specific functions (Note: direct flash access has been disabled) MicroPython


Library Function
pye Built-in text editor

2.3.1 - Display

The display module is available on platforms which have the framebuffer driver enabled. It allows for controlling the display of your device.

Available on:    ✅ CampZone 2020    ✅ Disobey 2020    ✅ CampZone 2019    ✅ HackerHotel 2019
Disobey 2019    ✅ SHA2017


Command Parameters Description
flush [flags] Flush the contents of the framebuffer to the display. Optionally you may provide flags (see the table down below)
size [window] Get the size (width, height) of the framebuffer or a window as a tuple
width [window] Get the width of the framebuffer or a window as an integer
height [window] Get the height of the framebuffer or a window as an integer
orientation [window], [angle] Get or set the orientation of the framebuffer or a window
getPixel [window], x, y Get the color of a pixel in the framebuffer or a window
drawRaw [window], x, y, width, height, data Copy a raw bytes buffer directly to the framebuffer or the current frame of a window. The length of the bytes buffer must match the formula width*height*(bitsPerPixel//8). This is a direct copy: color format (bitsPerPixel) must match the specific display of the badge this command is used on.
drawPixel [window], x, y, color Draw a pixel in the framebuffer or a window
drawFill [window], color Fill the framebuffer or a window
drawLine [window], x0, y0, x1, y1, color Draw a line from (x0, y0) to (x1, y1)
drawTri(angle) [window], x0, y0, x1, y1, x2, y2, color Draws a filled triangle
drawRect [window], x, y, width, height, filled, color Draw a rectangle at (x, y) with size (width, height). Set the filled parameter to False to draw only the border, or set it to True to draw a filled rectangle.
drawQuad* [window], x0, y0, x1, y1, x2, y2, x3, y3, color Draws a four-pointed shape between (x0, y0), (x1, y1), (x2, y2) and (x3, y3), always filled
drawCircle [window], x0, y0, radius, a0, a1, fill, color Draw a circle with center point (x0, y0) with the provided radius from angle a0 to angle a1, optionally filled (boolean)
drawText [window], x, y, text, [color], [font], [x-scale], [y-scale] Draw text at (x, y) with a certain color and font. Can be scaled (drawn with rects instead of pixels) in both the x and y direction
drawPng [window], x, y, [data or filename] Draw a PNG image at (x, y) from either a bytes buffer or a file
getTextWidth text, [font] Get the width a string would take if drawn with a certain font
getTextHeight text, [font] Get the height a string would take if drawn with a certain font
pngInfo [data or filename] Get information about a PNG image
windowCreate name, width, height
windowRemove name
windowMove name, x, y
windowResize name, width, height
windowVisibility name, [visible]
windowShow name
windowHide name
windowFocus name
windowList -
translate* [window], x, y Move the canvas of the window by (x, y)
rotate* [window], angle Rotate the canvas of the window by an angle (in randians)
scale* [window], x, y Scale the canvas of the window by (x, y)
pushMatrix* [window] Save the current transformation for later, may be more than one
popMatrix* [window] Restore the transformation pushed earlier, may be more than one
clearMatrix* [window], [keep-stack] Clears the current matrix, and also the matrix stack unless keep-stack is true
getMatrix* [window] Returns an array representing the current matrix for the window
setMatrix* [window], [matrix] Sets the current matrix to the array representing it
matrixSize* [window] Returns the current size of the matrix stack for the window

* This command is only available if you run a firmware with graphics acceleration, and the respective part enabled in the component config under Driver: framebuffer. Currently, badges have these features disabled by default.

flag platform description
FLAG_FORCE All Force flushing the entire screen.
FLAG_FULL All Force flushing the entire screen.
FLAG_LUT_GREYSCALE All with greyscale: SHA2017 Simulate greyscale.
FLAG_LUT_NORMAL All with e-ink Normal speed flush.
FLAG_LUT_FAST All with e-ink Faster flush.
FLAG_LUT_FASTEST All with e-ink Much faster flush.

Color representation

Colors are always represented in 24-bit from within Python, in the 0xRRGGBB format. This matches HTML/CSS colors which are #RRGGBB as well.

Devices with a smaller colorspace will not actually store the exact color information provided. For displays with a color depth of less than 24-bit the display driver will automatically mix down the colors to the available color depth. This means that even if you have a black and white display 0x000000 is black and 0xFFFFFF is white.


Setting one pixel

import display
x = 2
y = 3
display.drawPixel(x, y, 0x00FF00)  # Set one pixel to 100% green
display.flush() # Write the contents of the buffer to the display

Drawing a line

import display
display.drawFill(0x000000) # Fill the screen with black
display.drawLine(10, 10, 20, 20, 0xFFFFFF) # Draw a white line from (10,10) to (20,20)
display.flush() # Write the contents of the buffer to the display

Drawing a line using pixels:

import display, time
display.drawFill(display.BLACK) # Fill the screen with black before drawing the line
displau.flush() # Write the color to the screen before drawing the line
for i in range(80): # Loop for the X axis
    display.drawPixel(i, 1, 0x00FF00) # Set 1 pixel on the X axis i, and the Y axis 1 to 100% green 
    display.flush() # Write the pixel output to the screen
    time.sleep(0.050) # Sleep for 50 milliseconds as to show the line being drawn

Drawing text

import display
display.drawFill(0x000000) # Fill the screen with black
display.drawText(10, 10, "Hello world!", 0xFFFFFF, "permanentmarker22") # Draw the text "Hello world!" at (10,10) in white with the PermanentMarker font with size 22
display.flush() # Write the contents of the buffer to the display

Drawing a rectangle

import display
display.drawFill(0x000000) # Fill the screen with black
display.drawRect(10, 10, 10, 10, False, 0xFFFFFF) # Draw the border of a 10x10 rectangle at (10,10) in white
display.drawRect(30, 30, 10, 10, True, 0xFFFFFF) # Draw a filled 10x10 rectangle at (30,30) in white
display.flush() # Write the contents of the buffer to the display

Spinning a box

Note: as described earlier, matrix transformations are not enabled in the firmware by default

import display, math
# Note: radians are an angle unit where PI (math.pi) is half a rotation
display.clearMatrix() # Clear the matrix stack, just in case it wasn't already
display.translate(display.width() / 2, display.height() / 2) # Go to the middle of the screen
    # Everything is now offset as if the middle of the screen is X/Y (0, 0)
while True:
    display.drawFill(0xffffff) # Fill the screen with white
    display.rotate(math.pi * 0.1) # This will continually rotate the screen by a small amount
    display.drawRect(-20, -20, 40, 40, True, 0x000000) # Simply draw a rectangle, which will then spin
    display.flush() # Flush, show everything

Spinning text

Note: as described earlier, matrix transformations are not enabled in the firmware by default

Similarly to spinning a box, you can also spin text this way.

import display, math
# Note: radians are an angle unit where PI (math.pi) is half a rotation
text = "Well hello there!" # Whatever you want to show
font = "7x5" # Pick a font
scale = 2 # You can scale text, too!
display.clearMatrix() # Clear the matrix stack, just in case it wasn't already
display.translate(display.width() / 2, display.height() / 2) # Go to the middle of the screen
    # Everything is now offset as if the middle of the screen is X/Y (0, 0)
while True:
    display.drawFill(0xffffff) # Fill the screen with white
    display.rotate(math.pi * 0.1) # This will continually rotate the screen by a small amount
    textWidth = display.getTextWidth(text, font) # Get the size so we can center the text
    textHeight = display.getTextHeight(text, font)
    display.pushMatrix() # Save the transformation for later
    display.scale(scale, scale) # Scale the text
    display.translate(-textWidth / 2, -textHeight / 2) # Move the canvas so the text is centered
        # It is important you scale first, then translate
    display.drawText(0, 0, text, 0x000000, font) # Spinny texts
    display.popMatrix() # Restore the transformation
    display.flush() # Flush, show everything

More complex graphics

Note: as described earlier, matrix transformations are not enabled in the firmware by default

Now you’ve spun a box and some text, what about something more complicated?
Let’s say we draw a boat on a wave!

First, we draw the boat using some shapes:

import display, math

def drawBoat():
    display.translate(-4, 0) # Move just a little so the mast lines up nicely
    drawBoatMast(0x000000, 0x000000)

def drawBoatMast(mastColor, flagColor):
    # The points drawn, by place:
    # 0--1
    # |  |
    # |  6
    # |  |\
    # |  5-4
    # |  |
    # 3--2
    x0, y0 = 0, -23
    x1, y1 = 3, -23
    x2, y2 = 3, 0
    x3, y3 = 0, 0
    x4, y4 = 12, -10
    x5, y5 = 3, -10
    x6, y6 = 3, -20
    display.drawQuad(x0, y0, x1, y1, x2, y2, x3, y3, mastColor) # This is the mast: points 0, 1, 2, 3
    display.drawTri(x4, y4, x5, y5, x6, y6, flagColor) # This is the flag: points 4, 5, 6

def drawBoatBottom(color):
    # The points drawn, by place:
    # 0--------1
    #  \      /
    #   3----2
    x0, y0 = -20, 0
    x1, y1 = 20, 0
    x2, y2 = 16, 8
    x3, y3 = -16, 8
    display.drawQuad(x0, y0, x1, y1, x2, y2, x3, y3, color)

Now, to test your boat drawing action:

import display, math

# Put the boat drawing functions here

display.clearMatrix() # Don't forget
display.translate(30, 30) # Move to where you want to draw the boat
display.drawFill(0xffffff) # Clear the screen once more
drawBoat() # Draw the boat of course
display.flush() # Flush display; boat is now visible

Then, we’ll draw a wave and a sun:

import display, math

def drawSun(color): # Draws the sun with a circle and some lines
    display.translate(-3, -3 - display.height()) # This is where the sun will orbit around
        # We do - display.height() here because we set the origin to be at the bottom of the screen earlier
    display.drawCircle(0, 0, 30, 0, 360, True, color) # The sun
    for i in range(20): # Draw lines as the sun's rays
        display.rotate(math.pi / 10)
        display.drawLine(0, 35, 0, 45, color)

# For good measure.

display.translate(0, display.height())

sunOffset = 0
offset = 0
boatX = display.width() / 6
boatAngle = 0
boatY = 0

while True:
    drawSun(0x000000) # Draw the sun
    for i in range(display.width()): # Draw the waves by plotting points
        wave = math.sin((i + offset) * math.pi / 35) * 8 - 35
        display.drawPixel(i, wave, 0x000000)
        if i & 1:
            for j in range(round(wave - 1) | 1, 0, 2):
                display.drawPixel(i, j + ((i >> 1) & 1) + 1, 0x000000)
    offset += 8 # Move the waves over by a bit
    sunOffset += math.pi * 0.025 # Spin the sun by a bit

Finally, you can draw the boat on the wave, by adding some code:

while True:
    for i in range(display.width()):
        wave = math.sin((i + offset) * math.pi / 35) * 8 - 35
        display.drawPixel(i, wave, 0x000000)
        if i & 1:
            for j in range(round(wave - 1) | 1, 0, 2):
                display.drawPixel(i, j + ((i >> 1) & 1) + 1, 0x000000)
    # vvvv HERE vvvv
    display.pushMatrix() # Save the transformation, we're going to mess with it
    waterLevelBeforeBoat = math.sin((boatX + 2 + offset) * math.pi / 35) * 8 - 35
    boatY = math.sin((boatX + offset) * math.pi / 35) * 8 - 35
        # Calculate the two water levels, one at and one before the boat
        # By doing this, we know how and where to position the boat
    boatAngle = math.atan2(waterLevelBeforeBoat - boatY, 2) # Using atan2 to find the angle required to rock the boat with the wave
    display.translate(boatX, boatY - 6) # Now, position the boat
    drawBoat() # And draw the boat
    display.popMatrix() # Undo our changes to the transformation
    # ^^^^ HERE ^^^^
    offset += 8
    sunOffset += math.pi * 0.025

The source code for the boat can be found here: gist:

Available fonts:

The fonts in the latest firmware can be obtained from the sourcecode.

Known problems

  • Rotation of the contents of windows does not work correctly in combination with rotation of the screen itself
  • There is no method available to list the fonts available on your platform
  • There is no method for providing a custom font
  • There is no anti-aliassing support

2.3.2 - Buttons

The buttons API allows you to read the state of the buttons on a badge. This API encapsulates the drivers for different button types.

Badge support

This API is currently supported on the following badges:

  • SHA2017
  • Hackerhotel 2019
  • Disobey 2019
  • CampZone 2019
  • Disobey 2020
  • Fri3dcamp 2018

Support for GPIO buttons and touch-buttons via the MPR121 touch controller IC are supported. Touch buttons using the touch button features of the ESP32 can not be used (yet).


Command Parameters Description
attach button, callback function Attach a callback to a button
detach button Detach a callback from a button
value button Get the current value of a button
getCallback button Get the current callback of a button
pushMapping [mapping] Switch to a new button mapping
popMapping none Switch back to the previous button mapping
rotate degrees Adapt the button layout to an orientation. Accepts 0, 90, 180 and 270 as values.

Button availability per badge

Name SHA2017 Hackerhotel 2019 Disobey 2019 CampZone 2019 Disobey 2020 Fri3dCamp 2018 OpenHardwareSummit 2018
A Yes Yes Yes Yes Yes
B Yes Yes Yes Yes Yes
SELECT Yes Yes No No Yes
START Yes Yes No No Yes
UP Yes Yes Yes Yes Yes
DOWN Yes Yes Yes Yes Yes
LEFT Yes Yes Yes Yes Yes
RIGHT Yes Yes Yes Yes Yes

Default callback per button

Name SHA2017 Hackerhotel 2019 Disobey 2019 CampZone 2019 Disobey 2020 Fri3dCamp 2018 OpenHardwareSummit 2018
B Exit app Exit app
START Exit app Exit app Exit app

Callback implementation:

to use the buttons, you need to implement a callback function:

import buttons, display # Imports 2 libraries to use the buttons, the display library, as well as the buttons library
def on_action_btn(pressed): # Defines a function on_action_btn with the required parameter pressed
    if pressed: # Uses an if statement to check if the button has been pressed
        display.drawFill(display.BLACK) # If the button is pressed, sets the screen to black
        display.drawText(10,10,"Hack The Planet!!!", display.GREEN, "roboto_regular18") # Draws text if the button is pressed
        display.flush() # Flushes the screen to draw the text and color onto the screen
buttons.attach(buttons.BTN_A, on_action_btn) # Assigns the function on_action_btn to the A button

2.3.3 - System

The system API allows you to control basic features your app needs to provide a smooth experience to the user.


Command Parameters Description
reboot - Reboot the badge into the currently running app
sleep [duration], [status] Start sleeping forever or for the provided duration (in seconds). By defaut the function shows the fact that the badge is sleeping on the serial console, this can be disabled by setting the status argument to False.
start app, [status] Start an app. Optionally shows that an app is being started on the screen and in the serial console, for this to happen the status variable must be set to True.
home [status] Start the splash screen / default application. To show a message to the user set the optional status flag to True.
launcher [status] Start the application launcher. To show a message to the user set the optional status flag to True.
shell [status] Start a raw Python REPL prompt. To show a message to the user set the optional status flag to True.
ota [status] Initiate an Over-The-Air update session. Does NOT check if a newer firmware is available. To prevent hijacking other peoples badges it is NOT possible to provide a different update server or URL at this time.
serialWarning - Show a message telling the user that the currently running app can only be controlled over the USB-serial connection.
crashedWarning - Show a message telling the user that the currently running app has crashed.
isColdBoot - Returns True if the badge was started from RESET state. This function will only ever return true if the currently runing app was set as the default app.
isWakeup [timer], [button], [infrared], [ulp] Returns True if the badge was started from a WARM state. Normally this can be any warm state, however by setting the parameters specific wake reasons can be selected or ruled-out.
currentApp - Returns the name of the currently running app.


Starting an app

import system
system.start("2048") # Start the 2048 app (fails if this app has not been installed)

Going back to the launcher

import system

Going back to the homescreen

import system

Restarting the current app

import system

Sleep for 60 seconds, then return to the current app

import system

Querying the name of the currently running app

import system
appName = system.currentApp()
if not appName:
	print("This code is running either in the shell or in the boot context")
	print("Currently running app: "+appName)

2.3.4 - Appconfig

The appconfig API apps to register their user-configurable settings. By using this API, app settings are shown in the Settings page of the WebUSB website for supported badges.

Available on:    ✅ CampZone 2020


import appconfig

settings = appconfig.get('my_app_slug_name', {'option_1': 'defaultvalue', 'awesomeness': 1337, 'option_3': [1,2,3]})
mynumber = settings['awesomeness']


Function Parameters Returns Description
get app_slug_name, default_options Object Gets the user-set options for the app with the given name. If no configuration exists yet, returns the object passed into default_options.

2.3.5 - Audio

The audio API allows you to easily play audio files or stream URLs (.mp3, .wav, and modtracker .mod, .s3m, .xm). It is a wrapper around sndmixer, which can do much more but is a bit more verbose.

Available on:    ✅ CampZone 2020


import audio

channel_id ='/apps/myapp/doom.mp3', volume=150)


Function Parameters Returns Description
play filename_or_url, [volume], [loop], [sync_beat], [start_at_next], [on_finished] Channel ID (int) Play a file (e.g. ‘/apps/myapp/sound.mp3’) or stream from a url (e.g. ‘'). Filename or url needs to end with the filetype (.mp3, .wav, .mod, .s3m, .xm).

Use volume (0-255) to set the volume for this channel (defaults to system volume).

Use loop=True to repeat after playback is done.

Use sync_beat=(BPM of the music, e.g. 120) and start_at_next (1-32) to start playback at the next x-th 8th note (example: 1 starts at next 8th, 2 at next 4th (namely 2x an 8th), 4 at half note, 8 at whole note, 32 at whole bar).

If on_finished is a function, it is called when the playback ends.

Resources are automatically freed after playback finishes.
stop_looping channel_id - Cancel the looping status of a channel. This will end playback after the sound is finished with its current playback.
stop_channel channel_id - Cancel the playback of a channel immediately, and free its resources.

Known problems

  • Due to a bug in (presumably) our MicroPython version, stopping audio playback from a streaming URL causes a freeze in the MicroPython task. Therefore, you have to reboot your badge before you can play a different URL.
  • The current implementation can play around 4 wav files or 2 mp3 files at the same time without glitches or slowdowns. Any more can cause noticable artifacts.

2.3.6 - HID Keyboard & Mouse

The HID API allows you to make your CampZone 2020 badge act like a keyboard and mouse over USB. You can use it to type text, press key combinations, and click or move the mouse cursor.

Available on:    ✅ CampZone 2020


import hid, keycodes



Function Parameters Returns Description
keyboard_type text - Automatically sends the right key press and release events for the keys needed to type a text. Will use the SHIFT modifier for uppercase keys too. Blocks until the whole text has been typed.
keyboard_press_keys keys, [modifier] - Send key down commands over USB for the given keys. The optional modifier can be used to convey pressing ctrl, alt, shift, or the GUI/Windows button.
keyboard_release_keys - - Cancels all current key presses by sending a release command.

You can learn more in-depth about how this module works by checking out its source here

A more complex example

import hid, keycodes, time

# Presses ctrl+alt+delete
keys = bytes([keycodes.DELETE])
modifier = bytes([keycodes.MOD_LEFT_CONTROL & keycodes.MOD_LEFT_ALT])
hid.keyboard_press_keys(keys, modifier)

Known problems

  • The USB mouse interface is not yet present in the firmware at time of writing. A future Over-the-Air update will include it.

2.3.7 - Keypad

The keypad API allows you to call functions when someone presses a button on the silicone keypad of their CampZone 2020 badge.

Available on:    ✅ CampZone 2020


import keypad

def on_key(key_index, is_pressed):
    # Print to the serial port when a button is pressed or released
    print('Key ' + key_index + ': ' + is_pressed)



Command Parameters Returns Description
add_handler handler - Registers a handler function, that is called any time a keypad button is pressed or released. The first argument is the key index (0 top left, 3 top right, 12 bottom left, etc.), and the second argument is whether the button is currently pressed or not.
remove_handler handler - Removes previously registered handler function, so it won’t be called anymore.
get_state - touch_state Returns a list of 16 booleans indicating for each button whether they are currently pressed.

2.3.8 - MIDI Music Controller

The MIDI API allows you to make your CampZone 2020 badge act like a MIDI music controller over USB. You can use it to play music on your computer, or control music making programs like Ableton Live.

Available on:    ✅ CampZone 2020


import midi, time

midi.note_on(midi.CENTRAL_C+2) # D note (C plus two half tones)


Function Parameters Returns Description
note_on note, [velocity], [midi_channel] - Sends a note start command with the given optional velocity (“volume”, 0-127, default 127). You can change the MIDI channel if wanted (0-15).
note_off note, [velocity], [midi_channel] - Sends a note stop command with the given optional velocity (“volume”, 0-127, default 127). You can change the MIDI channel if wanted (0-15).

The CampZone 2020 hardware supports not only MIDI OUT, but also IN. This means you can receive messages from e.g. your audio program. Ableton Live uses this to command the LEDs on MIDI controllers. However, there is currently no Python API for this yet. It may be included in a future Over-the-Air update.

2.3.9 - Touchpads

The touchpads API allows you to call functions when someone presses a touchpad.

Available on:    ✅ CampZone 2020


import touchpads

def on_left(is_pressed):
    print('Left button: ' + is_pressed)

def on_ok(is_pressed):
    print('OK button: ' + is_pressed)

touchpads.on(touchpads.LEFT, on_left)
touchpads.on(touchpads.OK, on_ok)


Function Parameters Returns Description
on touchpad, callback - Set a callback that gets called whenever the given touchpad touch state changes. First argument to this function is the pressed state. Touchpad can be touchpads.LEFT, RIGHT, HOME, CANCEL, or OK.
off touchpad - Remove a previously set callback.

2.3.10 - ugTTS Text-to-Speech

The ugTTS API allows you to turn text into synthesized speech by querying Google Translate over WiFi. Either save it as an mp3 file, or play it directly. This module is based on the popular gTTS library.

Available on:    ✅ CampZone 2020


import wifi, ugTTS

if not wifi.wait():
    print('Oh no panic no WiFi')
    import system; system.launcher()

ugTTS.speak('This is a test')  # Plays over speakers
ugTTS.text_to_mp3('This is a test too', '/cache/test_speech.mp3')  # Saves to file for later playback

ugTTS.speak("Slaap kindje slaap", lang='nl') # Dutch
ugTTS.speak("Dommage", lang='fr', volume=100) # French and set volume


Function Parameters Returns Description
speak text, [lang], [volume] - Send piece of text to Google Translate and plays back the synthesized speech at given volume (0-255, default 255). You can optionally change the language, for values check gTTS library.
text_to_mp3 text, filename, [lang] - Same as speak() except it saves to the given filename.

Known problems

  • There is a finite length for the text before Google starts rejecting it.
  • We don’t expose the interface to set details like speech speed. Pull requests welcome.

2.3.11 - WiFi

The wifi API allows you to connect to WiFi networks easily.

Available on:    ✅ CampZone 2020    ✅ Disobey 2020    ✅ CampZone 2019    ✅ HackerHotel 2019
Disobey 2019    ✅ SHA2017


import wifi
wifi.connect() # Connect to the WiFi network using the stored credentials
if not wifi.wait():
	print("Unable to connect to the WiFi network.")
	print("You are now connected to WiFi!")


Function Parameters Description
connect [ssid], [password] Connect to a WiFi network. By default the stored credentials are used, but you can optionally provide the SSID (and password) of the network to connect to.
disconnect - Disconnect from the WiFi network.
status - Returns True if connected and False if not connected.
wait [timeout] Wait until a connection with the WiFi network has been made or until the timeout time is reached. Timeout is in seconds but may be provided in 10ths of seconds. If no timeout is provided the default timeout is used. Returns True if connected after waiting and False if a connection could not be made before the timeout.
ntp [only-if-needed], [server] Synchronize the Real-Time-Clock with the network. Normally the synchronisation is only started when the system clock has not yet been set since the last reset. This can be overruled by setting the only-if-needed parameter to False. By default the “‘” server pool is used.

Wait, is that all you can do with WiFi?!

No, of course not. The whole network API from the mainline MicroPython project is available on the firmware. Here are some examples for doing the stuff you’re probably looking for:

Connecting to a WiFi network, the hard way…

import network, machine, time

# First we fetch the stored WiFi credentials
ssid = machine.nvs_getstr("system", "wifi.ssid")
password = machine.nvs_getstr("system", "wifi.password")

# Create the station (WiFi client) interface
sta_if = network.WLAN(network.STA_IF)

# Activate the station interface

# Connect using the credentials
if ssid and password:
	sta_if.connect(ssid, password) # Secured WiFi network
elif ssid: # Password is None
	sta_if.connect(ssid) # Open WiFi network
	print("ERROR: no SSID provided. Please configure WiFi (or manually set the variables at the top of this example)")

wait = 50 # 50x 100ms = 5 seconds
while not sta_if.isconnected() and wait > 0:
	wait -= 1
	time.sleep(0.1) # Wait 100ms

if sta_if.isconnected():
	ip_addr, netmask, gateway, dns_server = sta_if.ifconfig()
	print("My IP address is '{}', with netmask '{}'.".format(ip_addr, netmask))
	print("The gateway is at '{}' and the DNS server is at '{}'.".format(gateway, dns_server))
	print("Failed to connect to the WiFi network.")

Scanning for networks

import network
sta_if = network.WLAN(network.STA_IF)
data = sta_if.scan()
for item in data:
	print("SSID: {}, BSSID: {}. CHANNEL: {}, RSSI: {}, AUTHMODE: {} / {}, HIDDEN: {}".format(item[0], item[1], item[2], item[3], item[4], item[5], item[6]))

Creating an access-point

import network
ap_if = network.WLAN(network.AP_IF)
ap_if.config(essid="badgeNetwork", authmode=network.AUTH_WPA2_PSK, password="helloworld") # Create a network called "badgeNetwork" with password "helloworld"

Note: if you get “unknown error 0x000b” after running the config command then the password you chose is too short.

More information

We used the loboris micropython fork (<- link) as the core of our badge firmware. The network API comes directly from his project.

The API looks a lot like the official MicroPython network API (<- link).

2.3.12 - Terminal

The term API allows you to make more advanced use of the serial console.


Command Parameters Description
goto x, y Move the cursor to (x, y)
home - Move the cursor to (1, 1)
clear - Clear the terminal
color [foreground], [backgrund], [style] Set the color used for writing to the terminal
header [clear], [text] Prints a header on the top of the screen. Optionally clears the screen. You can include your own text to be added after the device name.
menu title, items, [selected], [text], [item-width] Shows a menu with a specified title and menu-items. The selected menu item can be set. If not set the first item will be selected by default. Optionally a text can be provided which gets printed at the top of the menu screen. If the maximum string length of one of the menu options exceeds 32 characters a different length may be provided to make the menu options match in length. The fuction returns the location of the selected menu-item. This function is blocking.
prompt prompt, x, y, [buffer] Prompt for text to be entered by the user. The prompt will appear at (x, y) and before the prompt the prompt text will appear. If a buffer is provided the text buffer will contain the provided value. This function is blocking.
setPowerManagement pointer to the power-management task By providing a pointer to the power-management task running in your app this function will reset the timer to 5 minutes each time the user changes his selection in the menu shown by the menu() function. This was mainly intended as an internal function and a more refined version will probably be defined somewhere in the future…

2.3.13 - Machine

The machine API makes it possible to access certain hardware interfaces directly, allowing for example direct control of GPIOs, busses (I2C) and other interfaces.

This API is variation on the standard MicroPython machine API which has been extended and modified.

Not all features described in the official MicroPython documentation are available on the BADGE.TEAM platform firmware. And additionally some functions will differ in syntax from the official MicroPython for ESP32 firmware.

Non Volitile Storage (NVS)

The NVS functions allow for storage and retrieval of small amounts of data to be stored. This API is used to access WiFi credentials and other system information and can be used to manipulate system settings as well as for storing settings specific to your app.

Direct GPIO control

The Pin API can be used to directly control GPIOs of your badge.

I2C bus

The machine API for I2C allows you to control the system I2C bus of your badge, the I2C bus exposed on the SAO, Grove, Qwiic or other extension ports as well as a second I2C bus on any two broken out GPIOs of your choice.

SPI bus

Direct control over the SPI bus is currently not supported on the BADGE.TEAM platform firmware. Sorry! - Non Volatile Storage

This page describes the Non-Volatile-Storage (NVS) functions of the machine API. This NVS is used to store settings such as WiFi credentials and your nickname.

The NVS storage is a function of the ESP-IDF which allows for settings to be stored in a special partition on the flash of the ESP32 and is ment for small quantities of data. If you want to store large(er) amounts of data we suggest you use the filesystem functions of MicroPython to store your data on the FAT partition instead.


Command Parameters Description
nvs_set_u8 [space], [key], [value] Store an unsigned 8-bit value
nvs_get_u8 [space], [key] Retrieve an unsigned 8-bit value
nvs_set_u16 [space], [key[, [value] Store an unsigned 16-bit value
nvs_get_u16 [space], [key] Retreive an unsigned 16-bit value
nvs_setint [space], [key], [value] Store a signed integer value
nvs_getint [space], [key] Retreive a signed integer value
nvs_setstr [space], [key], [value] Store a string
nvs_getstr [space], [key] Retreive a string
nvs_erase [space], [key] Remove an entry from the NVS
nvs_erase_all [space] Remove all entries in a space from the NVS

NVS settings used by the firmware

The following list describes the settings stored by the BADGE.TEAM firmware.

Space Key Type Function
owner nick string The nickname of the owner of the badge
system default_app string The app/egg launched on powerup

NVS settings for your app

Please use the slug name of your app as the name of the space used to store your settings.



Reading the nickname

import nvs
nickname = nvs.nvs_getstr("owner", "nickname")
print("Your nickname is '{}'!".format(nickname))

Setting the nickname

import nvs
nvs.nvs_setstr("owner", "nickname", "") - The I2C bus

The machine API for I2C allows you to control the system I2C bus of your badge, the I2C bus exposed on the SAO, Grove, Qwiic or other extension ports as well as a second I2C bus on any two broken out GPIOs of your choice.

The ESP32 has two I2C controllers, each of which can be set to master or slave mode. Most of our badges use one of these I2C controllers for their internal I2C bus. You can take control over this system I2C bus using the machine API without directly causing issues but be adviced that doing this might possibly disrupt communications with one or more system components like the touch-button controller IC or the display.

Alternatively you can use the I2C API to define a secondary I2C bus on any two broken out GPIO pins.

Direct I2C access

The firmware contains a second API for working with the system I2C bus, allowing you to directly call some ESP-IDF I2C functions from within MicroPython.


Using the MicroPython machine.I2C API

While the directly exposed functions do already allow you to control i2c devices it is also possible to use the MicroPython I2C API on the same bus, simply by creating the bus using the exact settings used by the firmware itself.

The following snippet redefines i2c to be the MicroPython variant of the API instead of our direct functions. This snippet should work on all badges since it automatically uses the right pins for SDA and SCL as well as the correct bus speed for the board you are using.

import i2c, machine
i2c = machine.I2C(sda=machine.Pin(i2c.GPIO_SDA), scl=machine.Pin(i2c.GPIO_CLK), freq=i2c.SPEED)

If your board does not have a system I2C bus or if you want to use separate GPIOs for connecting your I2C device then you can also define a custom I2C interface on pins you choose. Keep in mind that the ESP32 can handle up to two I2C busses at once so if the firmare itself uses one then you can create only one custom i2c bus interface.

import machine
my_i2c_interface = machine.I2C(sda=machine.Pin(22), scl=machine.Pin(21), freq=100000) - Pin: direct GPIO control

Direct GPIO control

The machine.Pin API allows you to directly control GPIOs of the ESP32 on your badge.

Please check the schematics of your badge before using this API. If you configure the GPIOs of the ESP32 in a wrong way your might cause your badge to crash, stop responding or even permanently damage it. Be carefull!

Basic digital input

from machine import Pin
myInput = Pin(0) # GPIO0 (exposed as the "flash" button on most badges)
value = myInput.value()
print("The value of GPIO0 is {}.".format(value))

Basic digital output

from machine import Pin
myOutput = Pin(<GPIO NUMBER>, Pin.OUT) # Check the schematic of your badge to find the numbers which can be entered here
myOutput.value(True) # Set the pin state to 1 or "HIGH"



Pulse Width Modulation (PWM)


Wakeup from deep-sleep


2.3.14 - _buttons

2.3.15 - consts

2.3.16 - eink

2.3.17 - erc12864

2.3.18 - esp32_ulp

2.3.19 - espnow

2.3.20 - hub75

2.3.21 - keyboard

2.3.22 - mpu6050

2.3.23 - neopixel


The Neopixel library was reimplemented. Totally differently. Then we forgot to rename it. You can find some pointers on how to use it in the MCH2022 MicroPython docs

Sorry about that.

Import the library and start the driver

import neopixel

Sending data to the LEDs

Once you have enabled the driver you can start sending data. The driver expects a bytes object containing a byte per channel. The exact meaning of these bytes depends on the type of addressable leds your device uses. The easiest way to generate the needed bytes object is by converting a list into one by wrapping it with bytes().

import neopixel

ledData = [0xFF,0x00,0x00,0x00]

You can easily repeat patterns by using a simple Python trick: you can “multiply” a list by an amount to have python repeat the list that amount of times. The next example shows this, expecting 3 channels per led and 12 leds to be on the badge. If this is the case then all LEDs on the badge should light up in the same color.

import neopixel

ledData = [0xFF,0x00,0x00] * 12

Turning all LEDs off

import neopixel

number_of_channels = 3
number_of_leds = 12
ledData = [0x00] * number_of_channels * number_of_leds


2.3.24 - opus

Encoding data

To encode data, you have to know the sampling rate and number of channels and create an Encoder:

import opus
sampling_rate = 8000
stereo = False
encoder = opus.Encoder(sampling_rate, stereo)

Then you can use the encoder to encode audio frames. Those may have lengths of 2.5, 5, 10, 20, 40, or 60 milliseconds. Input data should be of type bytes or bytearray and contain 16-bit signed integers:

# One frame of data containing 480 null samples
input = bytearray(960)
# Encode the data, using at most 128 bytes for the frame. This would be around 2 kByte/s. At 8 kHz sampling rates, opus will use around 1 kByte/s for mono audio.
output = encoder.encode(input, 128)

Each encoded frame has some metadata at the beginning containing the channel, frequency, and the encoded size of the frame. This allows combining frames into one packet.

Decoding data

Decoders do not take any arguments with their constructor, because they take the necessary information from their input frames:

import opus
decoder = opus.Decoder()

The created decoder can handle any data created by opus.Encoder, even if the number of channels or the sampling rate differs - it will get reinitialized to match the new settings.

encoder = opus.Encoder(8000, 0)
decoder = opus.Decoder()

input = bytearray(960)
encoded = encoder.encode(input, 128)
decoded = decoder.decode(encoded)

2.3.25 - rgb

2.3.26 - samd

2.3.27 - sndmixer

(This page is still an ongoing effort and might contain mistakes.)

Starting the audio subsystem

The sound-mixer task runs on the second CPU core of the ESP32 and is able to mix together multiple auto streams and feed them to the DAC of your device.

The sound-mixer task needs to be started before any other audio related function can be used. Starting the sound-mxier task is done using the following API command:

import sndmixer
sndmixer.begin(<number-of-channels>, <enable-stereo>)

Starting the sound-mixer with only one channel and with stereo enabled (sndmixer.begin(1, True)) results in the best audio quality but does not allow you to play multiple audiostreams at the same time. We recommend you start the sound-mixer task with an amount of channels you actually plan on using.

Note: it is currently not possible to stop the sound-mixer task, change the number of channels or stereo mode without restarting your badge. We’re working on adding this functionality in the future.

Playing MP3 music

MP3 files can be played by reading the MP3 compressed sample data from a bytearray buffer or by reading from a stream by means of the file-descriptor.

Playing an MP3 file directly from a bytearray has the added benefit that you can play the MP3 file multiple times at the same time. This allows you to create basic soundboard effects where the same sample can be triggered while it is already playing.

Playing an MP3 file from a stream (fd) allows for playing larger files without loading them fully into ram before playing, this is usefull for background music as longer MP3 files can be played easily. This method can also be used to play MP3 streams directly from the internet.

Playing an MP3 file using the bytearray method

# Preparation: start the sound-mixer task
import sndmixer
sndmixer.begin(2, True)

# First we load the MP3 file into memory
with open("music.mp3", "rb") as fd:
  mp3data =

# Then we play the mp3 file
audioStreamId = sndmixer.mp3(mp3data)

# Now that the stream is playing we can set the volume (0-255)
sndmixer.volume(audioStreamId, 50)

Playing an MP3 file using the stream method

# Preparation: start the sound-mixer task
import sndmixer
sndmixer.begin(2, True)

# First we create a file-descriptor pointing to the MP3 file
mp3file = open("music.mp3", "rb")

# Then we play the mp3 file by passing the file-descriptor to the sound-mixer task
audioStreamId = sndmixer.mp3_stream(mp3file)

# Now that the stream is playing we can set the volume (0-255)
sndmixer.volume(audioStreamId, 50)

Playing opus-encoded data

Playback of opus works the same as MP3. You just have to replace mp3 with opus:

import sndmixer
sndmixer.begin(2, True)
sndmixer.opus_stream(open('snd.opus', 'rb'))

Opus data is expected to have frames formed like this: u8: channels | u8: sampling_rate / 400 | u16: len | u8[len]: data, where data is the actual opus-encoded data. This is the format produced by the opus module.

Playing tracker music

The sound-mixer can play mod, s3m and other tracker music files (your mileage may vary).

# Preparation: start the sound-mixer task
import sndmixer
sndmixer.begin(2, True)

# First we load the tracker music file into memory
with open("music.s3m", "rb") as fd:
  moddata =

# Then we play the tracker music file
audioStreamId = sndmixer.mod(moddata)

# Now that the stream is playing we can set the volume (0-255)
sndmixer.volume(audioStreamId, 50)

Playing wave files

This is ment for playing short sound effects or samples. You could even generate the samples using python if you wanted to!

# Preparation: start the sound-mixer task
import sndmixer
sndmixer.begin(2, True)

# First we load the wave file into memory
with open("music.wav", "rb") as fd:
  wavdata =

# Then we play the wave file
audioStreamId = sndmixer.wav(wavdata)

# Now that the stream is playing we can set the volume (0-255)
sndmixer.volume(audioStreamId, 50)


A (very) basic synthesizer is available as well. It currently generates sine, square and saw waves at a frequency and volume of your choosing. Each waveform generated uses one mixer channel.

# Preparation: start the sound-mixer task
import sndmixer
sndmixer.begin(3, True)

# Create the synthesizer channel
synthId = sndmixer.synth()

# Set the volume of the synthesizer channel
sndmixer.volume(synthId, 50)

# Set the frequency to 100Hz
sndmixer.freq(synthId, 100)

2.3.28 - ssd1306

2.3.29 - umqtt

2.3.30 - ussl

The ussl API provides low-level SSL encryption and certificate verification functions and is used by other APIs such as urequests.


Command Parameters Description
disable_warning Boolean: disable warning Disables the warning notifying users that the SSL connection isn’t secure because the server certificate isn’t verified
verify_letsencrypt Boolean: verify server certificate against Letsencrypt root Enables verification of the server certificate against the Letsencrypt root certificate
wrap_socket (See upstream Micropython documentation) (See upstream Micropython documentation)
debug_level 0-4 controls the amount of debug information printed

2.3.31 - voltages

2.4 - Jupyter Notebook

When coding the badge in (micro)python, it can be useful to use a Jupyter Notebook. This allows you to keep a ‘scratch pad’ of code snippets that you can easily and quickly adapt and run on the badge, without having to manually copy-paste code between your editor and the REPL all the time.

Normally a Jupyter Notebook would run the python code on your development machine. To make it run the code on your badge instead, you use the Jupyter MicroPython Kernel.

You can see a quick video of a notebook in action here.


This setup works best with Python 3. The easiest way to install is to create a virtualenv:

~$ mkdir badgehacking
~$ cd badgehacking
~/badgehacking$ python3 -m venv environment
~/badgehacking$ source environment/bin/activate

Install jupyter:

~/badgehacking$ pip install jupyter

Download and install the Jupyter MicroPython Kernel:

~/badgehacking$ git clone
~/badgehacking$ pip install -e jupyter_micropython_kernel
~/badgehacking$ python -m jupyter_micropython_kernel.install

If all went well, jupyter should now show micropython in the list of available kernels:

~/badgehacking$ jupyter kernelspec list
Available kernels:
  micropython    /home/aengelen/.local/share/jupyter/kernels/micropython
  python3        /home/aengelen/badgehacking/environment/share/jupyter/kernels/python3


To start the notebook, first enter the virtualenv again:

~$ cd badgehacking
~/badgehacking$ source environment/bin/activate

Start Jupyter:

~/badgehacking$ jupyter notebook

This should start the jupyter server on your machine, and open a browser window to interact with it. In that browser window, choose ‘New…’ and select ‘MicroPython - USB’. This will open a new MicroPython-enabled Notebook.

This will show a page with a ‘block’ that accepts python code. You can use Ctrl+Enter to execute the code in the block, and Alt+Enter to create a new block.

Before you can execute any commands, you will need to connect the notebook to your badge via the serial bus by adding the special command %serialconnect to a block and executing it. When you see Ready. the connection was succesful. On some badges you need to issue this command twice.


Currently, a disadvantage of the Jupyter Notebook over using the REPL directly is that code completion (tab completion) is not yet supported in the Jupyter MicroPython Kernel. Jupyter does support completion with other kernels, so it is likely possible to add this feature in the future.


The documentation for the Jupyter MicroPython Kernel is quite good.

3 - ESP32: firmware development

Getting started

New to development for the ESP32 platform firmware? This section will help you get up-and-running with firmware development.

Adding support for an ESP32 based device

Did you receive an ESP32 based badge at an event for which support is not yet available? Did you build your own ESP32 based device or badge? This section helps get you started with adding support for your badge.

Factory tests, provisioning and OTA updates

Interested in releasing a badge using our firmware? This section explains the factory testing and provisioning features, as well as how OTA updates and other release and support releated parts of our project work.

3.1 - Getting started

Welcome developer! This section describes how to get your development environment set-up so that you can build the BADGE.TEAM ESP32 platform firmware for any of the supported targets.


Our firmware has been set-up as an ESP-IDF project. The ESP-IDF is the development framework or SDK released by Espressif. Espressif is improving and updating the ESP-IDF constantly. Unfortunately these updates often introduce breaking changes. Because of this we have included the exact version of the ESP-IDF that we use as a submodule in our firmware GIT repository.

Downloading the project

You can clone the project by running git clone --recursive. Git will then create a folder called “ESP32-platform-firmware” containing the project files.

Installing required packages (Linux only)

Debian / Ubuntu: sudo apt-get install make unzip git libncurses5-dev flex bison gperf python-serial libffi-dev libsdl2-dev libmbedtls-dev perl

Installing the toolchain

Currently we’re using ESP-IDF version 3.2 to build the firmware. After cloning our repository you will find the exact version of the ESP-IDF used in the ESP32-platform-firmware/esp-idf folder. You don’t need to download or install a version of the ESP-IDF yourself.

The toolchain is another story: the newest ESP-IDF version (v4.x and newer) uses a different toolchain than the older (v3.3 / stable) version of the IDF. Because of this you can’t simply download the “latest greatest” ESP32 toolchain, instead you need to use a specific version.

You can download the correct version of the toolchain directly from Espressif using the following URLs:

Operating system Architecture Download link
Linux AMD64 (64-bit)
Linux I386 (32-bit)
Apple Mac OS OSX
Microsoft Windows WIN32

You can find the official toolchain installation instructions here:

The very, very short version of these instructions for Linux is as follows:

  • Extract the archive
  • Add the path to the bin folder in the archive, containing the toolchain executables, to your path export PATH="$PATH:/path/to/my/toolchain/xtensa-esp32-elf/bin"

Configuring the firmware

The firmware can be built for many different targets. Because of this the firmware has to be configured before it can be built. By default a “generic” configuration is used. Building the firmware using the generic configuration will get you to a Python shell on any ESP32 based device. However: almost all hardware support will be missing.

To apply the configuration for a specific badge navigate to the firmware/configs folder and overwrite (/create) the file firmware/sdkconfig with the configuration relevant to your badge.

(Note that running make clean to remove the build output is a bit broken / insufficient at the moment. Please remove the firmware/build folder manually after switching configurations to make sure the firmware is built correctly.)

Manually changing the configuration of the firmware is explained in the menuconfig section.

Building the firmware

After you’ve downloaded the firmware, applied the correct configuration and installed the correct toolchain you have to build the Micropython cross compiler. This extra compiler converts Python scripts into a Micropython specific binary format so that these Python scripts can be integrated inside the firmware image.

Building the Micropython cross compiler can be done by running bash from inside the firmware folder.

After you’ve built the Micropython cross compiler you can build the firmware by navigating to the firmware folder and running make. On multi-core systems compilation speeds can be improved by adding the number of threads to be used: make -j 4.

3.1.1 - Menuconfig

You can start menuconfig either by running ./ in the root of the repository or by executing make menuconfig in the firmware folder.

You will then be greeted by the main menu.

Main menu

The menu contains the following submenus:

  • SDK tool configuration
  • Bootloader config
  • Security features
  • Serial flasher config
  • Firmware & device configuration
  • Partition table
  • Compiler options
  • Component config

SDK tool configuration

In the SDK tool configuration menu you can configure the compiler toolchain path and prefix and the Python 2 interpretter to use. The default settings configured here use the toolchain found in your $PATH and the python2 executable found in your path as the Python interpretter. You should not have to change these settings.

Bootloader config

The bootloader config menu allows configuration of the bootloader. Changing these settings requires advanced knowledge of the ESP32 platform. The default values configured here should work.

Security features

The security features menu allows for configuring secure boot by encrypting the flash and signing the firmware. Use of these features on a badge would defeat the purpose of a hackable device and is thus not supported. Do not attempt to enable any of the options in this menu: you will brick your device!

Serial flasher config

This is the first interesting item in the list. In the serial flasher config menu you can configure the serial port to use when executing make flash, as well as the baudrate. This menu also allows you to tell the bootloader about the flash chip mode, speed an size. Most of the BADGE.TEAM badges have a 16MB flash chip, the CampZone2019 has a 8MB chip and most chinese boards have 4MB.

Firmware & device configuration

This menu allows you to configure the identity of your device.

Item Description
Code-name of the firmware Name of your device, lowercase and without spaces or special characters
Build number of the firmware Version information in the format “YYMMDDNN”: year, month, day and build
Name of the device Human readable name of the device
MicroPython modules directory subdirectory of /firmware/python_modules to use for the built-in MicroPython modules
Name of the badge on the app hatchery Name of your device or a compatible device supported by our Hatchery, lowercase and without spaces or special characters
Hostname of server for OTA updates Domain name of the server used for OTA updating (Example: “”)
Use HTTPS for OTA updates If enabled HTTPS can be used with a Letsencrypt SSL certificate. Other certificate authorities are not supported.
Port of server for OTA updates Port to use for OTA updates. Usually 443 for HTTPS or 80 for HTTP
Path on the server for OTA updates Path of the OTA firmware binary on the server, starting with a /
Path on the server for OTA update version Path to the JSON file with version information (used by the update check Python app shipped with some badges)
Hostname of server for app hatchery that contains user apps Domain name at which the Hatchery is hosted (used by the installer app shipped with some badges)
Default WiFi ssid The default WiFi SSID to use when the user has not yet configured WiFi
Default WiFi password The default WiFi password to use when the user has not yet configured WiFi (leave empty for ipen/insecure network)
Default display orientation For badges which use the same display as another type of badge but in a different orientation (explained below)

The HackerHotel 2019 badge is a direct derrivative of the SHA2017 badge, but with the display mounted in portrait mode instead of landscape. To allow for backwards compatibility with SHA2017 apps the real orientation has been set to landscape, while HackerHotel 2019 apps can call import orientation; orientation.default() to switch to the real orientation of the badge they are running on. The default orientation is configured here.

Partition table

In this menu a partition table can be selected. A set of partition tables has been provided in the /firmware/partitions folder. The partitions.ods spreadsheet can help you when designing a custom partition table. The partition table offset and the MD5 checksum options should be left at their default settings.

Compiler options

Advanced options for compiler optimization level and assertions. We suggest leaving the assertions enabled.

Component config

The component config submenu allows for configuring various components fo the firmware such as the MicroPython interpretter and the device drivers.

MicroPython configuration


Device driver configuration

The BADGE.TEAM firmware contains drivers for multiple devices such as displays and sensors. These drivers are written in C and part of the firmware itself, but they can be accessed from withing MicroPython using the bindings provided.

Is a menu empty? Does a feature not work?

  • To be able to use I2C based devices you have to enable the I2C bus driver first.
  • To be able to use SPI based devices you have to enable the VSPI driver first.
  • To be able to access displays using the “display” API from within MicroPython you have to enable the framebuffer driver.
  • The “double buffered” mode of the framebuffer driver is only relevant for devices which do not have their own buffer such as the Flipdot and HUB75 drivers. In all other cases it’s a waste of memory! Only enable this to use the display.flush() command with displays that stream directly from the framebuffer.

3.2 - Adding support

3.2.1 - Adding drivers

If you need low-level support for hardware that isn’t available yet, you can write your own drivers, and can expose them to the Python app layer.

  • Create the folder /firmware/components/driver_<category>_<name>.
  • In this folder, create files, Kconfig, and driver_<name>.c. Kconfig allows you to add configurable switches and variables from the ./ step. The driver source file exposes an initialisation function that will be called upon boot. Have a look at e.g. /firmware/components/driver_bus_i2c to see how to populate these files.
  • In /main/platform.c:platform_init(), add INIT_DRIVER(<name>) to have your driver actually initialise during boot.
  • Add your driver’s header directory to firmware/micropython/, e.g. MP_EXTRA_INC += -I$(PROJECT_PATH)/components/driver_<category>_<name>/include.
  • Add python bindings to your driver by creating components/micropython/esp32/mod<name>.c (see e.g. modi2c.c).
  • Tell micropython about your bindings by adding the following to firmware/micropython/
SRC_C += esp32/mod<name>.c
  • Add the following to components/micropython/esp32/mpconfigport.h to add the module symbols to the python environment (replace i2c with your name):
extern const struct _mp_obj_module_t i2c_module;
#define BUILTIN_MODULE_I2C { MP_OBJ_NEW_QSTR(MP_QSTR_i2c), (mp_obj_t)&i2c_module },
(to the define called MICROPY_PORT_BUILTIN_MODULES, add the following line after the other drivers):

3.3 - Factory tests and provisioning


3.4 - Device status

Badge Current OTA firmware Platform support status
SHA2017 ESP32 platform Build 50 (“Rise of skywalker”) Fully supported
Disobey 2019 Legacy SHA2017 firmware Unknown, OTA server is currently offline. Everything works, needs testing Not officially released yet
HackerHotel 2019 Legacy SHA2017 firmware Everything works but audio needs improvement Not officially released yet
CampZone 2019 ESP32 platform Fully supported
Disobey 2020 ESP32 platform Fully supported, audio support still in progress
Fri3dCamp 2018 ESP32 platform Proof of concept
OHS2018 ESP32 platform Display works, proof of concept
Odroid Go ESP32 platform Proof of concept. Display and audio work. No support for buttons or SD.
TTGO LoRa ESP32 platform Proof of concept. Basics of the LoRa radio work, no interrupts, no WAN.