Skip to the content
PCBKitNew board
2 layers64.2 × 50 mmShroomgon, the printed circuit board in 3D

Moon-themed Tildagon hexpansion app for the EMF badge (ESP32-S3) - 7x SK6812 LEDs, gyro-driven moon phase, auto-runs from an I2C EEPROM.

Size
64.2 × 50 mm
Copper layers
2
Thickness
1.59 mm
Parts
30
Nets
30
Pads
110
Tracks, vias
102, 22
Pours
4
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
16 Jul 2026
Updated
16 Jul 2026

Overview

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.

Main components

7

README

README.md

Shroomgon

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.

Files

Firmware/ is the app, Hardware/Shroomgon/ is the KiCad board. The commands below assume you are in Firmware/.

FilePurpose
app.pyThe app source (source of truth).
app.mpyCompiled bytecode — this is what gets flashed to the EEPROM.
prepare_shroomgon.pyRuns on the badge to write the header + format LittleFS.
test_shroomgon.pyStandalone CPython test of the gamma / power-clamp / animation maths.

Hardware

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-table expects tildagon.kicad_sym in a tildagon-base/ directory beside Shroomgon/, and fp-lib-table expects a tildagon.pretty/ footprint library inside it. Without them the symbols and footprints will not resolve.

What it does

  • Auto-launch: on insertion the badge OS mounts the EEPROM, imports the app, reads __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.
  • 5 effects × 5 palettes × 5 brightness levels, gamma-corrected. A matching moon phase is drawn on the screen in the current colour, with the app version under it.
EffectLEDsMoon
Shroomgonsweep follows the badge's spinfollows spin
Auto Moonsame sweep, on a 20 s clockcycles on the clock
Moonlightall LEDs breathe togethercycles on the clock
Rippleeach LED breathes on its own period, drifting out of phaseclock
Twinklerandom sparkle with decaycycles on the clock
  • Spin-driven moon: 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).
    • No IMU, or a read that throws, falls back to a slow timed sweep — never a crash.
  • Version on screen (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.
  • LEDs run in background_task — the light show keeps going even after you exit the screen; only a real unplug/stop (deinit) blanks them.
  • Power clamp (safety): ~20 mA per fully-lit channel, scaled so worst case stays under 450 mA. RGB full white = 420 mA; RGBW = 560 mA → clamped ~448 mA.
  • Never hangs: update/draw/LED writes guarded; degrades to screen-only if no board is found.

Controls (on-screen hints around the moon)

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.

ButtonActionHint position
UP (A)Install app onto the badge (/apps/Shroomgon)12 o'clock (Install)
CONFIRM (C)Next effect4 o'clock (next mode name)
DOWN (D)Brightness, 5 levels, cycles round6 o'clock (Bri n/5)
LEFT (E)Next colour palette8 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).

  • Front LEDs are 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.
  • The stored value is the pattern colour × 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.
  • An unlit LED (all channels 0) has no hue to match, so the last colour is held.

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.

Install / relaunch / reconnect

  • Install (UP button) copies the app off the hexpansion into /apps/Shroomgon, so it appears in the badge launcher menu and survives without the EEPROM.
  • Port auto-detect: launched from the menu (no 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.
  • Reconnect (no duplicates): a menu launch (no 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.

Which ports

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].

Multiple boards

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.

  • In sync: the timeline comes from time.ticks_ms(), not a per-instance counter, so all boards run the same phase with no shared state.
  • One screen: only the first instance to tick pushes itself to the foreground; the rest run LEDs-only. (Best effort — near-simultaneous mounts can stack two moons; press EXIT again.)
  • Buttons broadcast: effect, colour and brightness are set on every peer instance (found via scheduler.apps), so all boards switch together. The screen shows x<N> when more than one board is running.
  • Reconnect is menu-only: a launch with a config is a real board and always keeps its instance; only a menu launch (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.

Building app.mpy

The 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:

bash
# 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)

Flashing procedure (mpremote)

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 +).

bash
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.

Verify the header independently (re-read + checksum)

bash
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
"

Confirm auto-launch (hot re-plug)

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.

Configuring for board revisions

Top-of-file constants in app.py (rebuild app.mpy after changing):

ConstantMeaning
LED_PIN_INDEXWhich HS pin drives DIN. 0 = HS1 (config.pin[0]) … 3 = HS4.
NUM_LEDSNumber of SK6812 (default 7).
BYTES_PER_PIXEL3 = RGB SK6812, 4 = RGBW (drives the W channel).
MAX_BRIGHTNESS0..1 soft cap, before the power clamp. Applied after gamma.
BRIGHT_LEVELSMultipliers of MAX_BRIGHTNESS the DOWN button cycles.
BRIGHT_STARTIndex into BRIGHT_LEVELS the app boots at.
GYRO_AXISWhich 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_SCALEMoon turns per badge turn. Negate to flip the spin direction.
SPIN_STARTBoot angle in turns. 0.25 = half-lit.
SPIN_MAX_DTSeconds; frame gaps longer than this are ignored, not integrated.
AUTO_RATETurns/sec for the non-spin effects' moon. 0.05 = 20 s per cycle.
VERSIONShown 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_OFFSETNudges the port→front-LED mapping (port N → LED 2N by default).
POWER_BUDGET_MA / MA_PER_CHANNELPower-clamp budget / per-channel estimate.
VID / PIDMust 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.

Updating on an already-provisioned EEPROM

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).

Testing the maths without hardware

bash
python3 test_shroomgon.py

Stubs 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.

License

Split, because the hardware and the firmware have different ancestry:

Parts

30
QtyValueFootprintReferences
7SK6812MINI-ELED_SMD:LED_SK6812MINI-E_3.2x2.8mm_P1.5mm_ReverseMountD1, D2, D3, D4, D5, D6, D7
31uFCapacitor_SMD:C_0402_1005MetricC3, C5, C6
3TestPointTestPoint:TestPoint_Pad_D1.5mmTP1, TP2, TP3
210uFCapacitor_SMD:C_0603_1608MetricC2, C4
25.1kΩResistor_SMD:R_0402_1005MetricR1, R2
2MountingHole_PadMountingHole:MountingHole_2.2mm_M2_Pad_ViaH1, H2
1100nFCapacitor_SMD:C_0402_1005MetricC1
11kΩResistor_SMD:R_0402_1005MetricR3
1AP3602APackage_TO_SOT_SMD:TSOT-23-6_HandSolderingU2
1G***kibuzzard-6A4BD27Dkibuzzard-6A4BD27D
1G***kibuzzard-6A4BD2A3kibuzzard-6A4BD2A3
1G***kibuzzard-6A4BD300kibuzzard-6A4BD300
1Hexpansion-Connectortildagon:hexpansion-edge-connectorJ1
1M24C64-FMC6TGPackage_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.5mmU1
1Shroom3mmInventel:Shroom_FSilk_3mmG***
1SolderJumper_2_BridgedJumper:SolderJumper-2_P1.3mm_Bridged_RoundedPad1.0x1.5mmJP1
1SolderJumper_2_OpenJumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mmJP2

Questions

How big is the Shroomgon PCB?

It measures 64.2 × 50 mm and has 2 copper layers, 1.59 mm thick.

What parts does the Shroomgon use?

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.

Where can I download the Shroomgon KiCad files?

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.

Can I use the Shroomgon design in my own project?

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).

Similar boards

  • 2 layers50.8 × 38 mmGoogly eye hexpansion, the printed circuit board in 3D

    Googly eye hexpansion

    emfcamp/badge-2024-addons

    Addons for the EMF 2024+ Badges

    15 parts18CERN-OHL-P-2.0
  • 2 layers88 × 90.3 mmKeebdexpansion, the printed circuit board in 3D

    Keebdexpansion

    emfcamp/Keebdexpansion

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

    30 parts2MIT
  • 6 layers110 × 96 mmAssembly preview, the printed circuit board in 3D

    Assembly preview

    ripclap/rm520n-carrier

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

    446 partsCERN-OHL-P-2.0
  • 4 layers69.4 × 23.1 mmA1185 BMS Board, the printed circuit board in 3D

    A1185 BMS Board

    the-electron-vault/Apple

    172 parts12
  • 8 layers227.5 × 104 mmCom express-7-baseboard, the printed circuit board in 3D

    Com express-7-baseboard

    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.

    815 parts12Apache-2.0
  • 6 layers160 × 100 mmAwohali, the printed circuit board in 3D

    Awohali

    DeepSpace00/Awohali

    An open soure GNSS station for flash flood nowcasting

    556 parts
  • 4 layers50.7 × 40 mmP1 wireless indicator, the printed circuit board in 3D

    P1 wireless indicator

    Manul-Electronics/sp1_reader

    Standalone reader based on an ESP32 for P1 protocol

    42 parts
  • 4 layers50 × 50 mmWatch wearable, the printed circuit board in 3D

    Watch wearable

    TENNLab-UTK/NWRIST

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

    79 parts