
Googly eye hexpansion
emfcamp/badge-2024-addons
Addons for the EMF 2024+ Badges

Moon-themed Tildagon hexpansion app for the EMF badge (ESP32-S3) - 7x SK6812 LEDs, gyro-driven moon phase, auto-runs from an I2C EEPROM.
Shroomgon is an open-source printed circuit board by ramb0t, from the ramb0t/Shroomgon repository on GitHub, published under the MIT licence. It is a 2-layer board of 64.2 × 50 mm with 30 parts on 30 nets, routed with 102 tracks and 22 vias.
It is built around the AP3602AKTR-G1, M24C64-FMC6TG and SK6812MINI-E, with the TCC0402X5R105M6R3AT, CSA0603X5R106M6R3JT and CRCW04025K10FKEDC among its other parts. Its parts include 7 diodes, 6 capacitors, 3 resistors, 2 ICs and 1 connector. It belongs with the lEDs and displays boards and the ESP32 boards of this library.
Moon-themed Tildagon hexpansion app for the EMF 2024/2026 badge (ESP32-S3). Drives 7× SK6812 addressable LEDs and stores itself on an ST M24C64 8 KB I²C EEPROM (LittleFS). Auto-runs when the badge detects the board on insertion.
Built and verified against emfcamp/badge-2024-software @ 2775c86 (branch main),
running on a badge with MicroPython .mpy bytecode v6.3.
Firmware/ is the app, Hardware/Shroomgon/ is the KiCad board. The commands below
assume you are in Firmware/.
| File | Purpose |
|---|---|
app.py | The app source (source of truth). |
app.mpy | Compiled bytecode — this is what gets flashed to the EEPROM. |
prepare_shroomgon.py | Runs on the badge to write the header + format LittleFS. |
test_shroomgon.py | Standalone CPython test of the gamma / power-clamp / animation maths. |
KiCad 9 project in Hardware/Shroomgon/, built from the EMF Tildagon hexpansion
template. 7× SK6812MINI-E LEDs, an ST M24C64 EEPROM at 0x50 carrying the hexpansion
header and the app, and an AP3602A regulator. The mushroom outline and silkscreen are
generated by gen_mushroom.py; production/ holds gerbers, drills and the LCSC BOM.
Opening the project needs the Tildagon KiCad libraries, which are not vendored here.
sym-lib-tableexpectstildagon.kicad_symin atildagon-base/directory besideShroomgon/, andfp-lib-tableexpects atildagon.pretty/footprint library inside it. Without them the symbols and footprints will not resolve.
__app_export__, constructs it with the HexpansionConfig, and starts it.
The app emits RequestForegroundPushEvent (from its first background tick, after
it's registered with the scheduler) so it pops to the screen automatically.| Effect | LEDs | Moon |
|---|---|---|
Shroomgon | sweep follows the badge's spin | follows spin |
Auto Moon | same sweep, on a 20 s clock | cycles on the clock |
Moonlight | all LEDs breathe together | cycles on the clock |
Ripple | each LED breathes on its own period, drifting out of phase | clock |
Twinkle | random sparkle with decay | cycles on the clock |
imu.gyro_read()[GYRO_AXIS] gives rotation rate in deg/s.
Integrating it over each frame's dt turns the moon by the same fraction you turn the
badge — 360° of badge = one full moon cycle. Default axis is X (tilting the badge
over); set GYRO_AXIS to 1 or 2 for Y or Z (Z = spinning it flat, about the screen
normal). Reading the gyro rather than gravity means it works at any orientation, where
the accelerometer went blind whenever gravity lined up with the axis of rotation.
GYRO_DEADBAND (3 °/s) stops a resting badge creeping on sensor noise. Because the
angle is integrated, it has no absolute reference: the moon stays wherever you left
it, and any residual drift accumulates rather than self-correcting.SPIN_MAX_DT ignores frame gaps over 0.5 s, so a stalled loop can't teleport the moon.SPIN_START is the boot angle (0.25 = half-lit, not dark).VERSION, small and dim under the moon) and in the Install
message, so you can tell at a glance which build a board or an installed copy is running.background_task — the light show keeps going even after you exit
the screen; only a real unplug/stop (deinit) blanks them.Hexpansion buttons run A–F clockwise from 12 o'clock: A=UP, B=RIGHT, C=CONFIRM,
D=DOWN, E=LEFT, F=CANCEL. Each hint is drawn at its own button.
| Button | Action | Hint position |
|---|---|---|
| UP (A) | Install app onto the badge (/apps/Shroomgon) | 12 o'clock (Install) |
| CONFIRM (C) | Next effect | 4 o'clock (next mode name) |
| DOWN (D) | Brightness, 5 levels, cycles round | 6 o'clock (Bri n/5) |
| LEFT (E) | Next colour palette | 8 o'clock (next palette name) |
| CANCEL (F) | Exit to menu (LEDs keep running) | 10 o'clock (EXIT) |
Colours: Moon (cool white-blue), Amber, Spore (green), Rose, and Badge. The
palette tints the on-screen moon too. On RGBW strips (BYTES_PER_PIXEL = 4) only Moon
drives the W channel; the other palettes use RGB with W off.
Badge palette matches the badge's own front-panel LED — the one above your port — so the Shroomgon rides along with the default rainbow cycle (or whatever pattern is set).
tildagonos.leds[1..12], readable via ComposedNeoPixel.__getitem__.
Port N sits at LED 2N: the notification component places port N at
(2π/12)·(2N−1), which lands on that LED. PORT_LED_OFFSET nudges the mapping if a
board disagrees.pattern_brightness (0.1 by default), so a
raw read is ~10% of the real colour. We normalise the largest channel back to 255 —
taking the LED's hue and applying our own brightness. Downside: at 10% the channels
are quantised to ~25 steps, so the hue is approximate.Brightness: BRIGHT_LEVELS are linear multipliers of MAX_BRIGHTNESS (0.33), giving
0.01 / 0.09 / 0.17 / 0.25 / 0.33 → measured 0.5 / 11 / 21 / 31 / 41 mA per strip. Boots
at level 3 (BRIGHT_START). Level 1 is a deliberate near-off whisper — at 0.01 most
channels round to zero, so it reads as a dim tint rather than the palette colour.
/apps/Shroomgon,
so it appears in the badge launcher menu and survives without the EEPROM.HexpansionConfig passed), the app
scans I²C ports 1–6 for its own EEPROM (VID 0xCA75 / PID 0x534D) and builds the
config itself — so the LEDs still work from a menu launch while the board is
plugged in. No board → graceful screen-only.HexpansionConfig) detects an
already-running Shroomgon via scheduler.apps, re-foregrounds that instance, and stops
the new copy. This is the on-badge way to relaunch without unplugging — just pick it
from the menu. OS launches (config passed) never reconnect, so multiple boards each keep
their own instance — see Multiple boards.Works in any hexpansion port (1–6). No ADC needed — one high-speed pin to LED DIN
and the I²C EEPROM at 0x50. DIN pin comes from config.pin[LED_PIN_INDEX].
Up to 6 Shroomgons at once, any mix of ports. The badge OS launches one app
instance per hexpansion (App(config=HexpansionConfig(port))) and stops that instance
when that port is unplugged — so each board owns exactly one strip and its own
lifecycle. Unplugging one board blanks only its strip; the others keep animating.
time.ticks_ms(), not a per-instance counter, so
all boards run the same phase with no shared state.scheduler.apps), so all boards switch together. The screen shows x<N>
when more than one board is running.config=None) re-foregrounds a running instance and
stops itself. This is what keeps board #2 from being mistaken for a duplicate.Power is clamped per strip (450 mA), which matches the per-port 3.3 V rail. Measured peak is ~129 mA per strip at full brightness (Badge palette on a white front LED), so 6 boards are nowhere near any total.
Flash each board the same way, with PORT in prepare_shroomgon.py set to its slot.
They can share the same VID/PID/unique_id — the OS keys mounts by port.
Unplug the board whose moon is on screen and the OS stops that instance: the other boards keep animating, but the screen returns to the menu. Relaunch Shroomgon from the menu to reconnect to a still-running board.
app.mpyThe M24C64 (8 KB, 512-byte LittleFS blocks) leaves ~6.5 KB for one file; the full app is larger, so it ships as compiled bytecode (~6.2 KB, fits with only ~370 B spare — the empirical ceiling for a single file on this fs is 6568 B, so watch the margin).
.mpy bytecode is version-locked to the badge's MicroPython. This badge is mpy
v6.3; the compiled file tags no v6.1+ features (header 4D 06 00) so it loads on any
v6 runtime. Confirm your badge's version and build a match:
# on the badge REPL: import sys; print(sys.version); print(sys.implementation._mpy)
# _mpy low byte = mpy version (6); low 2 bits of the next byte = sub-version.
pip install mpy-cross # or ==<matching MicroPython version>
python3 -m mpy_cross app.py # -> app.mpy (pure bytecode, no -march)Plug the Shroomgon into a slot, connect over USB. Set PORT at the top of
prepare_shroomgon.py to the physical slot number (1–6). Adjust /dev/ttyACM* to
your device. Everything touching the runtime mount must be one mpremote session
(chained with +).
mpremote connect /dev/ttyACM0 run prepare_shroomgon.py \
+ fs cp app.mpy :/eeprom/app.mpy \
+ fs ls /eeprom \
+ exec "import vfs; vfs.umount('/eeprom')"prepare_shroomgon.py prints Header OK: (only if the checksum validates) plus the
partition block counts. fs ls shows app.mpy. Reformatting each flash keeps a single
file on the fs (no .py/.mpy import ambiguity).
If
could not enter raw repl: the running app holds the REPL.mpremote connect /dev/ttyACM0 repl, press Ctrl-C to>>>, Ctrl-] to quit, then rerun.
mpremote connect /dev/ttyACM0 exec "
from machine import I2C
from system.hexpansion.util import detect_eeprom_addr, read_hexpansion_header
i2c = I2C(2) # <-- your PORT
addr, alen = detect_eeprom_addr(i2c)
print(read_hexpansion_header(i2c, addr, addr_len=alen)) # raises on bad checksum
"Unplug and re-plug the hexpansion. The badge shows a "Shroomgon" notification,
mounts the EEPROM at /hexpansion_<PORT>, and the app pops to the screen with LEDs
animating. mpremote connect /dev/ttyACM0 fs ls /hexpansion_2 confirms the file.
Top-of-file constants in app.py (rebuild app.mpy after changing):
| Constant | Meaning |
|---|---|
LED_PIN_INDEX | Which HS pin drives DIN. 0 = HS1 (config.pin[0]) … 3 = HS4. |
NUM_LEDS | Number of SK6812 (default 7). |
BYTES_PER_PIXEL | 3 = RGB SK6812, 4 = RGBW (drives the W channel). |
MAX_BRIGHTNESS | 0..1 soft cap, before the power clamp. Applied after gamma. |
BRIGHT_LEVELS | Multipliers of MAX_BRIGHTNESS the DOWN button cycles. |
BRIGHT_START | Index into BRIGHT_LEVELS the app boots at. |
GYRO_AXIS | Which gyro axis turns the moon. 0=X (tilt), 1=Y, 2=Z (flat spin). |
GYRO_DEADBAND | °/s below which gyro output counts as noise and is ignored. |
GYRO_SCALE | Moon turns per badge turn. Negate to flip the spin direction. |
SPIN_START | Boot angle in turns. 0.25 = half-lit. |
SPIN_MAX_DT | Seconds; frame gaps longer than this are ignored, not integrated. |
AUTO_RATE | Turns/sec for the non-spin effects' moon. 0.05 = 20 s per cycle. |
VERSION | Shown under the moon and in the Install message. Bump it when you reflash. |
PALETTES | (name, (r, g, b)) colours the LEFT button cycles. Index 0 is the moon; a None colour means "live", read from the front-panel LED. |
PORT_LED_OFFSET | Nudges the port→front-LED mapping (port N → LED 2N by default). |
POWER_BUDGET_MA / MA_PER_CHANNEL | Power-clamp budget / per-channel estimate. |
VID / PID | Must match prepare_shroomgon.py (used by port auto-detect). |
This board routes DIN to HS1 = pin 12 →
LED_PIN_INDEX = 0. If a revision moves DIN to another high-speed pin, update the index.
Tuning brightness: measured peak at level 5/5 is ~71 mA on the fixed palettes, and
~129 mA on Badge if the front LED goes full white (vs the 450 mA budget). Ripple is
the hungriest effect, since its LEDs can drift into alignment near full. Huge headroom
either way: raise MAX_BRIGHTNESS freely if the top level still looks dim; the clamp
keeps enforcing the ceiling.
Simplest: re-run the flashing procedure (it reformats — fine for a single-file app).
To overwrite without reformatting, mount the existing partition and fs cp in one
session (see git history / earlier notes).
python3 test_shroomgon.pyStubs the badge-only modules, imports the real app.py, and checks gamma is
monotonic/bounded, every effect frame stays under the 450 mA budget, full white (RGB
and RGBW) is clamped, and the brightness cap holds.
Split, because the hardware and the firmware have different ancestry:
Hardware/Shroomgon/) — CERN-OHL-P v2, see
Hardware/Shroomgon/LICENSE.txt. It derives from the
EMF Tildagon hexpansion template, which is CERN-OHL-P; the modifications are recorded in
Hardware/Shroomgon/NOTICE.md per subsection 3.3(b).| Qty | Value | Footprint | References |
|---|---|---|---|
| 7 | SK6812MINI-E | LED_SMD:LED_SK6812MINI-E_3.2x2.8mm_P1.5mm_ReverseMount | D1, D2, D3, D4, D5, D6, D7 |
| 3 | 1uF | Capacitor_SMD:C_0402_1005Metric | C3, C5, C6 |
| 3 | TestPoint | TestPoint:TestPoint_Pad_D1.5mm | TP1, TP2, TP3 |
| 2 | 10uF | Capacitor_SMD:C_0603_1608Metric | C2, C4 |
| 2 | 5.1kΩ | Resistor_SMD:R_0402_1005Metric | R1, R2 |
| 2 | MountingHole_Pad | MountingHole:MountingHole_2.2mm_M2_Pad_Via | H1, H2 |
| 1 | 100nF | Capacitor_SMD:C_0402_1005Metric | C1 |
| 1 | 1kΩ | Resistor_SMD:R_0402_1005Metric | R3 |
| 1 | AP3602A | Package_TO_SOT_SMD:TSOT-23-6_HandSoldering | U2 |
| 1 | G*** | kibuzzard-6A4BD27D | kibuzzard-6A4BD27D |
| 1 | G*** | kibuzzard-6A4BD2A3 | kibuzzard-6A4BD2A3 |
| 1 | G*** | kibuzzard-6A4BD300 | kibuzzard-6A4BD300 |
| 1 | Hexpansion-Connector | tildagon:hexpansion-edge-connector | J1 |
| 1 | M24C64-FMC6TG | Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.5mm | U1 |
| 1 | Shroom3mm | Inventel:Shroom_FSilk_3mm | G*** |
| 1 | SolderJumper_2_Bridged | Jumper:SolderJumper-2_P1.3mm_Bridged_RoundedPad1.0x1.5mm | JP1 |
| 1 | SolderJumper_2_Open | Jumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mm | JP2 |
It measures 64.2 × 50 mm and has 2 copper layers, 1.59 mm thick.
30 parts, built around the AP3602AKTR-G1, M24C64-FMC6TG, SK6812MINI-E and TCC0402X5R105M6R3AT. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
From the ramb0t/Shroomgon repository on GitHub, at the commit this page was made from: the board is Hardware/Shroomgon/Shroomgon.kicad_pcb and its schematic Hardware/Shroomgon/Shroomgon.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Its repository publishes it under the MIT licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).

emfcamp/badge-2024-addons
Addons for the EMF 2024+ Badges

emfcamp/Keebdexpansion
A keyboard hexpansion for the Tildagon badge using the solder.party KeebDeck Keyboard. Software for the hexpansion is provided by @sodoku.

ripclap/rm520n-carrier
KiCad RM520N-GL carrier with 2.5 GbE, passive PoE, dual SIM interfaces and independent LoRa management.


antmicro/com-express-7-baseboard
This project contains open hardware design files for a baseboard supporting COM Express 7 modules. The board breaks-out major communications and peripheral interfaces provided by the standard.


Manul-Electronics/sp1_reader
Standalone reader based on an ESP32 for P1 protocol

TENNLab-UTK/NWRIST
Code and KiCAD PCB design for the NWRIST: Neuromorphic Wearable for Real-Time Intelligent Sensing Technology paper