HivePaaS
An open-source, self-hosted PaaS on Docker Swarm. Deploy apps, databases and functions from one VPS to a whole cluster, with SSL, backups and autoscaling built in.
One no_std Rust binary. A static plugin registry behind one button. Apps from a clock to a transformer that writes stories on-device. Signed firmware over the mesh — even to the WiFi-less nodes. Six b
Developer toolsRust · ShellGitHub @jpheinOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
A fleet firmware platform that fits in 400 KB — no_std Rust, ESP-NOW mesh with crown election, signed over-the-air updates, and five ESP32 targets (C3, C5, C6 watch, S3) from one tree
Taken from the repository README
Loading the full README.
Read the full README ↗A platform that fits in 400 KB. One no_std Rust firmware, a registry of apps behind a single button, and a routerless mesh that elects its own gateway and updates itself over the air — now running across five targets on four ESP32 chip families.
It started on a $3 ESP32-C3 SuperMini with a 0.42" (72×40) OLED, as "can we make this into a tiny game player? can it run Minecraft?" Answer: real Minecraft, no (400 KB RAM vs. gigabytes) — but the soul of it, yes. From that joke it grew into something genuinely unusual: a fleet of $1–$3 boards running one binary, talking to each other directly over ESP-NOW (no router, no cloud), that you can update over the air with signed firmware — even the WiFi-less nodes, over the mesh — and that hosts a living creature which hops from board to board. Games, a shared-world MMO, a native Home Assistant integration, remote config, observability, and OTA — all on a chip that costs less than a coffee.
Then the platform outgrew the chip. The same source tree now compiles for RISC-V and Xtensa, drives a 72×40 monochrome OLED and a 2.8" colour touchscreen, and the boards on both ends of that range speak the same SMOLv1 wire format to each other. See the five targets.
One creature lives on the whole fleet. It inhabits a single board at a time — showing its mood, hunger, and growth on that OLED — and when you unplug the board it's on, it hops to a neighbour over the mesh and carries on living there. Non-holder boards show a Weasley-clock pointer toward wherever the Familiar currently is; you can greet it, call it, and feed it. Exactly-one-holder arbitration, migration on loss, and orphan re-election are all handled in the mesh layer (crate::familiar + the SMOLv1 FAM frame). Human-verified on glass — pull the plug, watch it jump. (#57 — merged, PR #99.)
A shared-world creature that migrates across $3 microcontrollers when you pull power is, as far as we know, one-of-a-kind for a
no_std-Rust ESP-NOW fleet.
🌐 Live site: https://jphein.github.io/smol/ · 🕹️ Hardware-verified on real boards (the bench fleet).
targets/ is the roster — one folder per board, each with a machine-readable target.toml that declares its chip, its firmware flavor, and whether it produces a download. Adding a folder + manifest adds a target; nothing else has a list to maintain.
Two firmware flavors share the tree. The fleet flavor is rust/clock/ — the no_std esp-hal binary this README is mostly about (apps, SMOLv1 mesh, signed OTA, election). The GUI flavor is targets/c6-watch/ — a Slint/Embassy touch-UI workspace, subtree'd from the esp32c6-watch repo, that speaks the same SMOLv1 frames and today carries three board arms (board-waveshare-c6, board-cyd-c5, board-esp32s3-cyd). Some boards run one flavor; the S3 runs both.
| target | chip | flavor | status |
|---|---|---|---|
c3 — the headless node, ~$1 | ESP32-C3 · RISC-V | fleet | 🟢 shipping — the reference fleet. Every other board's numbers are measured against it. |
c3-oled — the node with a face, ~$2.76 | ESP32-C3 · RISC-V | fleet | 🟢 shipping — the same image as c3; the 72×40 SSD1306 is simply answered at boot. |
s3-cyd — 2.8" ILI9341V + capacitive touch | ESP32-S3 · Xtensa | fleet and GUI | 🟢 glass-verified (2026-08-26) — boots, paints, takes touch, meshes, and reaches NTP + MQTT + Home Assistant, in both flavors, on real hardware, and took build 345 → 1405 over the air the same day: the first cross-architecture OTA in smol's history — by WiFi self-fetch off the per-chip staged line, not a mesh receive (#518). Still not building in CI — the only blocker left is the runner toolchain. |
c6-watch — the Waveshare AMOLED smartwatch | ESP32-C6 · RISC-V | GUI | 🟢 shipping in its own repo, in-tree here as a full-history subtree. Live on the mesh; rust/clock compiles clean for the chip but does not yet link a fleet image for it. |
c5-cyd — the NM-CYD-C5 2.8" touch board | ESP32-C5 · RISC-V | GUI today | 🟡 mesh-proven — the first non-C3 silicon ever heard on smol's mesh (2026-08-24). For rust/clock a linked fleet image now on hardware (#485, 2026-08-27) — so it is past — but it is : there is still no measured memory budget row, so no floor gate. Zigbee-bridge role back-burnered. |
How to read those labels, because the difference has cost real time here: checks-only means cargo check is clean for the chip — a real claim, and a much weaker one than builds. Builds means CI can produce a linked, budget-gated artifact. Glass-verified means a human watched it work on the bench. The declarations live in tools/build-matrix.toml (checks ⇐ builds ⇐ ships, enforced by a checker in both directions, so a stale pessimistic row fails the gate exactly like a stale optimistic one) and in rust/clock/src/budget.rs, and the two must agree or the build fails.
Which board does what, and where the honest edges are. This is the short version — the full matrix (69 rows × 6 board-flavors — capabilities plus build/release posture, every non-✅ cell carrying its reason and its issue) is docs/CAPABILITIES.md.
A board can run either firmware flavor or both, and the two are genuinely different machines, so
parity is judged per flavor — the framing targets/s3-cyd/PARITY.md
established. ✅ integrated · 🔶 partial · 🛠 hardware supports it, firmware doesn't yet · ❌ the
silicon or the fitted hardware cannot · 📋 planned · — n/a to this flavor.
The GUI flavors are a superset of the fleet node (JP's ruling, #473): every fleet feature is in scope on a touch board too, plus the GUI layer. So a fleet feature the GUI flavor hasn't implemented is a gap with an issue number, never "N/A by design" — "the scenes are compile-time" explains why a gap is hard, not why it isn't one. ❌ is reserved strictly for hardware that cannot. Every 🔶 and 🛠 below carries its issue; the cell is that issue's acceptance test.
| c3 | c3-oled | s3 · fleet | s3 · GUI | c6 · GUI | c5 · GUI | |
|---|---|---|---|---|---|---|
| SMOLv1 mesh membership | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Crown election / gateway | ✅ | ✅ | ✅ | 🔶 observe-only #278 | 🔶 observe-only | 🔶 #488 |
| Gateway: WiFi + NTP + MQTT → HA | ✅ | ✅ | ✅ | ✅ | ✅ | 🔶 HA dark #488 |
| HA discovery + Update entity | ✅ 🔶 no _soc #498 | ✅ | ✅ | 🛠 own topics #492 | 🛠 own topics #492 | 🛠 #492 |
| Keyed-CFG (every knob, no reflash) | ✅ 11 keys | ✅ | ✅ | 🛠 only S/U/R #490 | 🛠 #490 | 🛠 #490 |
| Custom screens | ✅ | ✅ | ✅ | 🛠 not rendering #473 | 🛠 #473 | 🛠 #473 |
| Mesh-OTA receive | ✅ | ✅ | 🔶 the 345→1405 roll was a WiFi self-fetch, not a mesh receive (#518) | 🔶 compiled #495 | 🔶 unproven on the C6 #495 | 🔶 #495 |
| Mesh-OTA relay / serve | ✅ | ✅ | 🔶 client only | 🔶 #495 | 🔶 #495 | 🔶 #495 |
| HTTP / push OTA | ✅ | ✅ | ✅ | ✅ | ✅ zero-touch | 🔶 #488 |
| Ed25519 signed-image verify | ✅ both paths | ✅ | ✅ | 🔶 mesh yes, HTTP no #489 | 🛠 build-monotonicity only #489 | 🛠 #489 |
| Display | ❌ no panel fitted | ✅ SSD1306 72×40 | ✅ ILI9341V, 4× scaled | ✅ ILI9341V | ✅ CO5300 AMOLED | ✅ ST7789 |
| Touch | ❌ none fitted | ❌ | 🛠 no fleet driver (GUI ⊇ fleet, not the reverse) | ✅ FT6336U | ✅ FT3168 | 🔶 XPT2046, poll-only |
| Status LED | ✅ |
Three things that table is careful about. The Bard runs on glass and is not in any shipped
image, on any board: it is budget-predicated, and no chip's declared row currently has the 39,072 B
of DRAM it needs, so it builds only under the off-fleet waiver — which the packaging path then
refuses to publish. ❌ never means "not done": on the C3's display it means no panel is fitted, and
on the C6's LE Audio it means forensically closed — that silicon has no ISO link layer, so no
earbud model changes it. And where a whole column is 🔶 for one reason — the C5's is — the reason is
that nobody has watched it, not that it fails; budgets and behaviours here are measured, never
inherited.
Every fleet-flavor board runs the unified Rust firmware (rust/clock/, no_std esp-hal) — one binary, a BOOT-button menu, static plugin dispatch, no heap on the base build. The blue LED shows ESP-NOW peer state in the background (off → blink = detected → solid = connected). Which plugins are present is a build tier, not a runtime setting, and the tiers stack: the cheapest board and the fleet gateway run the same code paths with a different slice compiled in. (The touch boards' GUI flavor carries its own app set — see targets/c6-watch/README.md.)
| App | What | Status |
|---|---|---|
| The Mesh Familiar | a living creature that migrates across the fleet as boards come and go (see above) | 🟢 on glass — migration verified (#57) |
| The Bard | a real transformer LLM (260K-param TinyStories, int8, XIP from flash) that narrates without end, fully on-device — a sliding-window KV cache means it just keeps going, typewriter-style on the 72×40 OLED. Only 5 lines × 14 chars are on the glass at once, so the screen is a window the story moves past, which is what makes an unbounded tale possible on 400 KB of RAM. Every node has its own protagonist (id8 tells owl tales); the opening (CFG-T, #303) and the pace + inf/page delivery (CFG-V, #302) are settable per node from Home Assistant with no reflash | 🟢 on glass — #300: 67-token story, 202 ms/tok, port proven bit-for-bit vs an independent Python reference. #302: endless mode measured on id8 at 224–274 ms/tok (max 281 — 10–35% slower than bounded, attention now spans the full window), V changed 160→120 ms/char mid-narration with no restart, and stack high-water byte-identical at 55,440 B across 5 consecutive reports spanning many tale boundaries — the window does not creep. #303 merged + HW-verified. Fleet not rolled, and inf is near-continuous compute so it is the worst-case power draw — no battery-life claim implied |
| World Snake (MMO) | shared 256×256 toroidal world over the mesh, scrolling viewport, peers drawn by name, mesh leaderboard, 6 treasure-powers | 🟢 flashed + running fleet-wide (#5) |
| Marauder's Watch | every node shows where every other node is, by ESP-NOW roster RSSI (near/far EWMA — no BLE) | ✅ merged (#58) |
| Treasure Hunt | RSSI warmer/colder game over the mesh | ✅ merged (#60) |
| Custom screen | per-node user-defined text/entities, authored from the HA dashboard (HA resolves {entity} refs to plain text; the leaf just renders bytes) | ✅ merged (#45) |
| HA Batt / HA Grid |
Mesh time-sync (loop-free, newest-NTP-wins) and magical realm names — a pure function of the node id, so two boards agree on what to call each other with no handshake (via realm-sigil, corpus from lexicon's fleet group; uniqueness across all 256 ids is a compile-time guarantee — a colliding namespace does not build). The mapping is derivable; the id↔board assignment is not — see BUILDING.md run under all of it; the boot splash shows the sigil version name.
This is where smol stops being a toy. The elected gateway briefly bursts onto WiFi to reach Home Assistant; the rest are ESP-NOW-only leaves the gateway serves.
(origin, msgid, frag) seen-set, table-free so it rides roam/re-election for free) and its telemetry reaches home through a neighbour; the battery/grid downlink flows back the other way behind a strictly-newer freshness gate. Escalation is hysteretic (3 consecutive un-ACKed messages to latch, 2 direct-ACK probes to un-latch), so the ordinary all-hear case stays byte-identical to single-hop. 🟢 hardware-proven — on 2026-07-14 a gateway-deaf smol delivered telemetry home through a neighbour: the first routed frame in smol's history. Honest v1 limits: uplink ACK is best-effort and a stranded leaf's channel-scanning caps throughput. Both honest-v1 follow-ups have since closed: #126 (latched-leaf channel parking) and #124 (the UP2 observability envelope). (builds on the #76 election.)SMOLv1 CFG <id><KEY><value> frame carries every per-node knob over the mesh, all editable from the HA dashboard, no reflash: default screen (#21, key S), LED mode (#48, L), display units °F/°C + 12/24h (#43, U), plugin visibility (#55, P), Custom screen layout (#45, Y), broker override (#100, B), OTA-host override (#100, O), remote reboot (#52, R, transient), WiFi scan trigger (#71, W, transient), the IO pin-map + output control (#72, keys G/g — wire a digital sensor/button/relay/LED to any free GPIO and drive it, all from HA), and the (#303, — validated leaf-side against the model's own 512-token vocabulary). Most apply live; edge-triggers a reboot; / are one-shot and never cached. ✅ merged. rust/clock/ — the unified Rust firmware (no_std esp-hal, the fleet flavor): apps + the ESP-NOW mesh (src/net/), the Familiar (src/familiar/), the Bard (src/bard/), OTA (src/ota.rs + src/ota_mesh.rs), Cast (src/net/cast.rs), the per-chip memory budgets (src/budget.rs)targets/ — the target roster and the artifact matrix: one folder per board, each with a target.toml manifest. targets/c6-watch/ is a subtree of the esp32c6-watch repo and carries the GUI flavor's whole workspaceblockdigger/, games/snake/, games/snake-2p/ — Arduino/C++ games (U8g2 + Bluepad32)watch/ — Arduino smartwatch starter · oled_test/ — I²C + display sanity checkexperiments/ — pocketwatch/ (3D-printable case generator + STLs), atomic14-games/, nes-c3/, case-mod/ha/ — the Home Assistant integration (MQTT packages + dashboard) · tools/ — OTA publish + reproducible-build + image-verify scriptssite/ — the editable project website (tiny Python server + WYSIWYG; auto-deploys to GitHub Pages)docs/ — research + guides (below)tools/status_check.sh) · start hereexperiments/pocketwatch/ generates a parametric round case — body + lid + crown (3 printable STLs) — in the classic orientation: a chain bail and a removable crown covering the USB-C port at the top; the OLED, buttons and clear-PLA LED light-pipes below; pockets for the board + a 502030 LiPo + a TP4056. The OLED is rotated 180° in firmware so it reads upright when hung.
Prebuilt images live on the releases page. Per-target downloads have landed (#413): the nightly release carries five images — smol-c3, smol-s3-cyd (fleet), smol-s3-cyd-gui, smol-watch-c6 and smol-c5-cyd-gui. tools/release_targets.sh walks the targets/*/target.toml manifests and packages every target declaring artifact = true (fleet) or gui_artifact = true (GUI), through the same reproducible-build calls the OTA publish path uses; the two Xtensa flavors ride a job that provisions the espup esp toolchain. Each artifact ships a NOTES.md beside it carrying its sha256, its chip's stack-floor provenance in plain words, and the (chip, profile) sha-lineage rule. Which board gets which flavor is the capability matrix.
# Verify against the sha256 printed in the image's own NOTES.md — every artifact
# carries it inline. If the release also carries a combined SHA256SUMS, use that:
sha256sum <image>.bin
sha256sum -c --ignore-missing SHA256SUMS # --ignore-missing matters: you downloaded
# ONE image and the file lists them all
# ⚠️ If this board has EVER taken an OTA, clear otadata first, or the flash silently
# lands in the slot the bootloader will not select. This spares nvs (and your node id).
espflash erase-region --port /dev/ttyACM0 0xf000 0x2000
espflash write-bin --port /dev/ttyACM0 0x0 <image>.bin
Then check the Loaded app from offset line — it names the slot that actually ran.
🔑 Two rules that surprise everyone
1. Downloads are for NEW hardware. Fleet boards update over signed mesh OTA, never from GitHub. The ed25519-signed
smol/ota/stagedpath is the only sanctioned way a deployed board changes firmware.2. Published images carry PLACEHOLDER credentials — including the mesh group key — on purpose, so the artifact stays byte-reproducible. A downloaded board boots, drives its display and runs the menu, the games and the Bard; it will not join WiFi, reach a broker, or talk to an existing mesh. And a key baked into a public binary is public: two boards flashed from the same download share a mesh key anyone can extract. To own your mesh, re-key — regenerate
GROUP_KEYinrust/clock/src/secrets.rs(32 random bytes, start fromsecrets.rs.example), rebuild, reflash. Placeholder-key boards can mesh only with other placeholder-key boards, and can never join a re-keyed fleet. (#394.)Full detail: docs/RELEASES.md.
See docs/BUILDING.md. TL;DR: the Rust firmware builds with cargo build --release --features espnow (exactly one chip feature per invocation — the triple and the features are chosen together) and flashes with espflash; Arduino games flash with arduino-cli (esp32:esp32:esp32c3). The Xtensa S3 arm needs the espup esp toolchain. Run the site locally with python3 site/server.py.
The original: ESP32-C3 SuperMini + 0.42" OLED (AliExpress) — and four more in targets/, each folder documenting its own hardware truth.
Built collaboratively with Claude Code — a fleet of agents did the research, flashing, CAD, and firmware while the build stayed in motion.
AGPL-3.0-or-later © 2026 Jeffrey Pine Hein. See LICENSE.
Third-party subtrees keep their own licenses and notices: targets/c6-watch (MIT OR Apache-2.0, from waveshare-watch-rs), its vendored i-slint-renderer-software (GPL-3.0-only OR Slint licenses), experiments/nes-c3/Anemoia-ESP32 (GPL-3.0) and experiments/atomic14-games (Unlicense).
buildsc3-tapstone-gw — the Tapstone arena's USB-serial gateway | ESP32-C3 · RISC-V | fleet tapstone-gw tier | 🟡 built + host-verified; the C3 itself not yet on hardware (#548) — @TS1 lines on USB ⇄ MATCH frames on the mesh, WiFi off so it can never be the crown. The CI-gated arm. No download: flashed by hand onto one board. |
s3-tapstone-gw — the same gateway on an S3 | ESP32-S3 · Xtensa | fleet tapstone-gw tier | 🟢 bench-verified (2026-09-26) — two S3 shrine boards passed the gateway bench: 100/100 frames, p95 ≤ 17.2 ms, mac_ok 100/100. Same code as the C3's; a declared hand build ([hand_build] in tools/build-matrix.toml), since CI cannot build Xtensa. |
| ✅ |
| ✅ WS2812/RMT |
| 🛠 pin only #491 |
| ❌ none fitted |
| 🛠 pin only #491 |
| smol Cast → WLED | 🔶 | ✅ | 🔶 mirror blank #483 | 🔶 default-off #493 | 🔶 #493 | 🔶 #493 |
| Mesh Snake (head-to-head) | ✅ | ✅ | ✅ | 🛠 #494 | 🛠 #494 | 🛠 #494 |
| The Bard (on-device transformer) | 🔶 on glass, off-fleet — 6,720 B short #497 | 🔶 same | 🔶 14,400 B short #497 | 🔶 opt-in | 🔶 30,480 B short #497 | 🔶 |
| Audio out | ❌ no codec | ❌ | 🛠 codec ACKs, no path #476 | 🔶 acoustic proof pending #477 | ✅ | 🛠 header un-scoped #486 |
| Audio in / voice | ❌ | ❌ | 🛠 mic fitted #476 | 📋 phase 2 #478 | ✅ ES7210 + STT | 🛠 #486 |
| Battery % | ❌ | ❌ | 🛠 ADC on GPIO9 #479 | 🛠 declared, no reader #479 | ✅ AXP2101 | 🛠 #486 |
| PMU · IMU | ❌ · ❌ | ❌ · ❌ | ❌ · ❌ ⚠️ log lies #480 | ❌ · ❌ | ✅ · ✅ | ❌ · ❌ |
| Die temp (TSENS) | ✅ | ✅ | ❌ soc_has_tsens false | ❌ | ✅ | ❌ |
| 802.15.4 (Zigbee/Thread) | ❌ no radio | ❌ | ❌ no radio | ❌ | 📋 mutually exclusive with ESP-NOW | 📋 back-burnered #399 |
| BLE | 🔶 silicon has it; refuted in practice #22 | 🔶 | 🔶 not built | 🔶 | ✅ GATT | 🔶 |
CI builds a linked image | ✅ | ✅ | 🛠 no espup on runners #413 | 🛠 | 🛠 needs widen_rom_region | 🛠 no budget row #485 |
| Published download | ✅ | ✅ by design use the c3 image | ✅ | ✅ | ✅ | ✅ |
| Stack-floor provenance | ✅ Derived | ✅ Derived | 🔶 ObservedSufficient #484 | 🔶 #484 | 🔶 BootAssert | 🛠 none of its own #485 |
| live battery voltages + SOC (big per-battery pages) and grid power on every display, mirrored from Home Assistant over MQTT + re-broadcast to leaves as mesh frames |
| 🟢 on-glass round-trip verified on the gateway (#16/#17) · 🟡 the leaf leg is inferred, not observed — protocol.md logs no leaf-side BATT/GRID receipt, and the fleet is all-gateway so the path is unexercised |
| smol Cast | stream a board's display to a network WLED matrix as realtime UDP pixels | 🟢 HW-verified (#26) |
| Clock · Snake · Mesh Snake · Benchmark · atomic14 pack | NTP clock, one-button Snake, 2-board head-to-head, a live ESP-NOW link tester, and 5 single-button games | 🟢 flashed |
| Block Digger | Minecraft-ish dig/build with a Bluetooth Stadia controller (Bluepad32; the Arduino build) | 🟢 flashed |
TBRWN WiFi-slot switch shipped with #100 and was retired by #142 — the fleet is single-network now and a received N is drained and ignored.)identifiers:["smol<id>"], name:"smol <id> <Noun>" and sw_version. That makes the registry a self-reported fleet manifest — ids, sigil names and running builds, with no list to maintain anywhere. Plug in a board and it appears; re-provision one and it renames itself. The sigil is read from the device name rather than recomputed, deliberately: the naming formula already changed once (#218) and stranded four names in the docs. ✅ merged.G binds a button, contact, relay or LED to any of the free GPIOs (0/1/3/7/10) — no reflash, no NVS wear — with output states held across reboot via CFG-g. This is the dollhouse per-room lamp + button foundation (#75). ✅ merged.N is drained and ignored; the net record's slot/fallback bytes are reserved-zero). Don't cite the slot switch or its auto-revert as a current feature.net=/brk=/otah=), and the applied-config echo (cfg=) that HA compares against its command topics to flag config drift — the display-mirror (the gateway OLED as a live HA image), and bound-input press counters (io=, #72) — so a silent rollback, a drifted node, or a doll's button push is visible in HA at a glance. ✅ merged.SOURCE_DATE_EPOCH), so an image's sha256 is a verifiable identity you can check against a board before/after a flash. ✅ merged. Note the scope: shas are comparable within one (chip, profile) pair — the S3 builds at a different opt-level to work around an LLVM issue, and that legitimately produces different (equally correct) bytes.esp-rtos async executor beside the superloop: main is #[esp_rtos::main] async fn, embassy-time's driver comes from esp-rtos, and evidence-bearing radio sends have learned bounded async waits instead of spinning on a callback that may never fire. The motivation is a measurement, not an aesthetic: on a two-board bench, a held WiFi window that left the mesh deaf for ~15 s under the blocking transport left it deaf for 169 ms under the async one — ~89×, against a 279 ms ambient floor. ⚠️ Read the scope of that number honestly: it was taken during the port campaign's Phase 2, with less running than the fleet image carries, and re-running the harness against the merged tree is still an open item. 🟢 merged (PR #391). Step T — moving the seven (controller, station) transport pairs to one declared owner — is scoped in docs/embassy/T-SCOPE.md and in progress, not landed.T-SCOPE.md