Get Project ↗
// features
💡
RGB "eye" LEDs
Two WS2812 LEDs with a runtime colour/style/brightness engine (Rainbow/Red/Green/Blue × None/Breathe/Crazy × 25/50/75/100%), configured from the LED submenu.
📡
Badge-to-badge IR interaction
A received NEC frame beeps 3× and shows a "someone's looking for you" note. On by default; toggleable from the menu. Sending from the menu blinks the arm LED.
🚩
CTF challenges
An on-badge Konami-code "Easy" challenge and a snake-style "Hard" game, plus hidden serial-only challenges.
🖥️
On-device menu UI
On the e-paper display.
🤖
AI Interactive Mode
A host AI drives the badge over the CH340 UART using a line-based JSON protocol.
🔊
Buzzer / motor / arm LED effects
Including a few built-in songs.
📱
CTF registration QR
Shows a QR (registration URL + badge serial) for staff.
// menu & buttons
ButtonPinRole
BUTTON1PB12UP
BUTTON2PB13DOWN
BUTTON3PB14SELECT
BUTTON4PB15CANCEL / BACK / EXIT

Menu structure:
Main: AI · CTF · LED · IR · Motor · Music
CTF → Register · Challenges
Challenges → Easy (Konami) · Hard (game)
LED → Eye LED · Arm LED
IR → IR Send · IR Recv · IR Interact

// hardware & firmware

Pin Map
(STM32U073CBT6, LQFP48)

FunctionPin
Buzzer PWMPB0
IR emitter (NEC TX)PB1
Vibration motor PWMPB3
Arm backlight LED PWMPB4
Eye WS2812 dataPB5
IR receiver (NEC RX)PB11
Button 1 — UPPB12
Button 2 — DOWNPB13
Button 3 — SELECTPB14
Button 4 — CANCEL / BACKPB15
Debug / AI UART RXPA3
Debug / AI UART TXPA2
E-paper displaySPI

Unique ID

A unique 8-byte serial is derived from the STM32U073's 96-bit UID via FNV-1a and embedded in the CTF registration QR code.

// building & flashing

SWD programming header

The badge is programmed over SWD with an ST-LINK. The SWD pads are on the back of the badge, on the left side of the body, in a single vertical column. From top to bottom:

Order (top → bottom)Signal
1GND
2SWCLK
3SWDIO
4+3V

Wire these to the matching pins on an ST-LINK (V2/V3), then flash with the Arduino IDE, arduino-cli, or STM32CubeProgrammer.

Unlocking RDP (Readout Protection)

The badge MCU ships with RDP Level 1 enabled and password-protected, so once the ST-LINK is wired up you must unlock it before you can flash. Use STMicroelectronics' STM32CubeProgrammer CLI (STM32_Programmer_CLI) with the RDP password below.

RDP unlock password:

0x52484320 0x56316330 0x6E33204C 0x34385237

Unlock procedure — run the following commands in order (STM32CubeProgrammer over SWD):

# 1. Provide the RDP password on access port 0 (locks/authenticates AP0)
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG ap=0 -lockRDP1 0x52484320 0x56316330 0x6E33204C 0x34385237

# 2. Set RDP to level 1 (0xBB) — regression/handshake step
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -ob RDP=0xBB

# 3. Provide the password on access port 1 to unlock RDP
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG ap=1 -unlockRDP1 0x52484320 0x56316330 0x6E33204C 0x34385237

# 4. Drop RDP back to level 0 (0xAA) — full read/write access restored
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -ob RDP=0xAA

⚠️ Warning: Regressing RDP from Level 1 to Level 0 (RDP=0xAA) triggers a full mass erase of the flash. Back up anything you need before unlocking, and re-flash the firmware afterwards.

Building from source

1. Install the Arduino IDE (or arduino-cli) with the STM32 core (STM32duino).
2. Select the board: Generic STM32U0 series, variant GENERIC_U073CBTX.
3. Install the required libraries:
   — Adafruit NeoPixel — WS2812 eye LEDs
   — Arduino-IRremote (4.x) — NEC send/receive
   — QRCode (ricmoo qrcode.h) — CTF registration / flag QR codes
4. Open rhc_badge/rhc_badge.ino and compile/upload over SWD or the built-in bootloader.

Adafruit NeoPixel needs a one-line patch — see Patching Adafruit NeoPixel below. Without it, the first (left-eye) WS2812B latches a stuck random colour.

Patching Adafruit NeoPixel (WS2812B first-pixel fix)

The WS2812B-V6 eyes need a small patch to Adafruit NeoPixel 1.15.5's STM32 bit-bang path. Without it, the first LED in the chain (left eye) latches a stuck random colour at power-up (usually green/blue) and can't be turned off, because the first data pulse is emitted slightly distorted and misread as a 1 on the green MSB. (Downstream LEDs re-shape the signal, so only the first is affected.)

Fix: hold the data line low and align to a SysTick reload boundary for ~20 µs before the first bit. Edit the installed library file …/Arduino/libraries/Adafruit_NeoPixel/Adafruit_NeoPixel.cpp, in the STM32 800 kHz section (the ARDUINO_ARCH_STM32 block, ~line 2882) right after SysTick->VAL = 0; and before for (;;) {, insert:

// WS2812B-V6 first-pixel workaround: force data LOW for ~20us (16 bit
// periods) and align the first rising edge to a reload boundary, so the
// first pulse width isn't distorted and the first pixel latches correctly.
LL_GPIO_ResetOutputPin(gpioPort, gpioPin);
for (uint8_t w = 0; w < 16; w++) {
  while (SysTick->VAL > t0) ;
  while (SysTick->VAL <= t0) ;
}

Notes:

Flashing the prebuilt binary

A prebuilt image of the initial release is included as rhc_badge.ino.bin, so you can flash the badge without building from source. Load it at the flash base address (0x08000000) with STM32CubeProgrammer over SWD:

STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -d rhc_badge.ino.bin 0x08000000 -v

If the target still has RDP Level 1 enabled, unlock it first — see Unlocking RDP above. Programming a fresh .bin does not by itself clear readout protection.

Build-time feature switches (top of rhc_badge.ino)

MacroDefaultEffect
ENABLE_LED_MOTOR_LOCKOnForbids an LED and the vibration motor being on at the same time
ENABLE_IDLE_LED_FXOn"Worn badge" idle effect: short random-colour eye blink every 5 min
ENABLE_MCU_DEEP_SLEEPOffDeep-sleeps the MCU when idle (disabled by default — IR interaction needs the CPU)
ADMINOffBuilds an admin badge with an extra privileged "IR Send – ADMIN" item
// ai interactive mode

Serial Protocol

Entered from the main menu (or the host holds the link). Commands are plain-text lines over the 115200-baud UART, interpreted only in AI mode. Lines may be prefixed with a numeric request id (echoed back). Responses are single-line JSON. At boot, the badge emits: {"event":"boot","version":"2.0.0"}

CommandSyntax / subcommands
pingping{"pong":true}
infoName, version, capability list
statusCurrent motor/arm/buzzer/IR/buttons/epaper state
eyescolor <r> <g> <b> · color2 <lr lg lb rr rg rb> · effect <name> · bright <pct> · off
buzzerbeep [freq] [ms] · tone <freq> <ms> · song <rowboat|scale|alarm> · off
motor<0..100> · off
arm<0..100> · off
irsend [addr] [cmd] · recv <on|off>
displaytext [size x y] <text…> · image <0|1> · clear · sleep
imagebegin · data <hex…> · raw <len> · show · peek <off> [len] — stage/show a full 200×200 frame
regionbegin <x> <y> <w> <h> · data <hex…> · show — partial-region blit
buttons<on|off> — stream button press/release as JSON events
exitLeave AI mode

Notes:

// ctf challenges

Note: the firmware contains a hardening theme around keeping certain secrets (the admin IR frame and the sysmem key window) unreadable through the debug/AI surfaces, including flash-alias folding so the same bytes can't be read via a mirror address.

⚠️ Spoilers: a full solution walkthrough — including the maze rule, the image peek OOB key leak, and the RSA break — lives in writeup/, with runnable solver scripts and a firmware dump under writeup/solve/.

// power management

The e-paper is bistable, so the image persists without power. After 30s of inactivity the badge enters a standby screen and sleeps the panel; the next button press wakes and redraws it. Eye LEDs stay off in standby to keep coin-cell drain low. IR interaction (on by default) keeps the CPU running so incoming NEC frames can be decoded.

// source layout
PathPurpose
rhc_badge/rhc_badge.inoMain firmware: UI, LEDs, IR, buzzer/motor, AI protocol
rhc_badge/epd1in54_V2.{cpp,h}, epdif.{cpp,h}1.54" e-paper driver + interface
rhc_badge/epdpaint.{cpp,h}Framebuffer drawing primitives
rhc_badge/fonts.h, font8/12/16/20/24.cBitmap fonts
rhc_badge/imagedata.{cpp,h}Built-in images (QR, etc.)
rhc_badge/ctf_q1.{cpp,h}, ctf_q2.{cpp,h}CTF challenge assets/logic
rhc_badge/demo_path.hDemo path for the Hard-challenge game
rhc_badge.ino.binPrebuilt image of the initial release (flash @ 0x08000000)
image/rhc-badge-front.jpg, rhc-badge-back.jpgBadge photos shown in the README
writeup/CTF solution walkthrough (write-up, slides, solvers) — spoilers
// get in touch

Contact

Questions about the badge, firmware, or the CTF it unlocks?

Email
robotichackingcommunity
@proton.me
Speaking, sponsorship, CTF inquiries
X / Twitter
@RobotHackComm
Announcements and updates