Firmware project that turns a small ESP32 touchscreen into a local controller for a La Marzocco Micra espresso machine — over Bluetooth, with no cloud dependency for day‑to‑day use.
Set your brew temperature, flip the steam boiler, put the machine on standby, watch the boiler come up to temperature, and (with a supported Bluetooth scale) run a live shot timer and flow graph — all from a dedicated little screen next to the machine instead of a phone app.
The focus is on local control via bluetooth. Currently internet is only used for optional NTP.
Contents: Features · Getting started · Screenshots · Supported hardware · 3D prints · Developer documentation · Credits · License
- Local Micra control over Bluetooth — brew and steam temperatures, steam boiler on/off, standby, and live machine status. No cloud, no phone app.
- Bluetooth scales — Bookoo Themis, Acaia (Umbra, Lunar, Prochef, Pyxis) and Varia Aku: live weight, shot timer, flow graph, tare, and the scale's own settings from the screen.
- Brew by weight — stop the shot at a target weight on boards wired into the paddle circuit, or detect shots from the weight stream alone on any board. Finished shots freeze into a review graph.
- Group care — auto‑flush after the cup lifts, flush on demand, and a backflush cleaning cycle, on wired boards.
- Warm‑up chime — boards with a speaker play a tune when the machine is ready, so you can start it and walk away.
- Shot history — every shot is saved to a microSD card as CSV, browsable on the device's Stats tab or from a phone via the device's own web page.
- Settings backup — preferences copy to the same card and restore onto replacement hardware.
- Over‑the‑air updates — the device checks for new firmware and installs it on request, with an optional beta channel.
- Setup from your phone, no app — pairing and Wi‑Fi credentials go in through a page the device serves itself; scan the QR code on its screen.
- Automatic time — NTP over your Wi‑Fi with a timezone picker, saved to the on‑board RTC where present.
- Made to live on the counter — themes, °C/°F, 12/24‑hour clock, brightness, idle‑screen artwork; layouts from 4.3" to 8" panels.
Everything keeps working if the machine, the scale, or Wi‑Fi is absent — the UI just shows that part as offline.
Important
Before buying any hardware, confirm you can get your machine's Bluetooth token. Apollo authenticates to the Micra with a token issued by the La Marzocco cloud. Grab it first with the LM Token app — sign in with your La Marzocco account and copy the token. If it comes back blank, the same tool can provision one over Bluetooth. Checking now avoids a nasty surprise after a board is already on your bench.
Pick a board from the Which board? table below — three of them come as finished boxes that need no assembly at all. A Bluetooth scale is optional but unlocks the shot timer, flow graph, and brew‑by‑weight. Wiring the machine's paddle (for Auto shot) is optional too, and can always be added later — see the wiring guide.
No-toolchain option: the web flasher
flashes any supported board straight from Chrome, Edge, or Firefox over USB —
pick your board, click Install. Upgrading this way keeps your paired machine,
Wi‑Fi and settings (unless you choose "Erase device"). Prebuilt images also live
on the Releases page — those
are full images, so flashing one with esptool does clear saved settings.
After the first flash Apollo updates itself over WiFi: it checks for new firmware, shows what changed, and installs on a tap. Tick Include pre-releases on the flasher (or turn on Settings → Apollo → WiFi → Beta updates on the device) to run beta builds instead — testing welcome, and a build that won't boot rolls back on its own.
Building from source requires PlatformIO (pio) and
a USB cable.
make flash # print selection of flash options
make flash-p4-5 # or target a board: p4-5 | p4-4-3 | s3-4-3c | p4-x-8
make monitor # open the serial console (115200 baud)Settings → Micra → Bluetooth → Scan, then pick your machine. The device saves it, then asks for the machine's Bluetooth token (step 4) — the token is issued by the La Marzocco cloud and can't be read off the machine, so there's a short one‑time step to fetch it.
Tap Enter token on the prompt (or Settings → Micra → Set up) to start the
device's own Wi‑Fi access point, Micra-Setup. Scan the QR code on the
device's screen with your phone's camera — it joins the access point and the
setup page pops up on its own. (Or join Micra-Setup manually and open
http://192.168.4.1.) Paste your token and Save — the device connects and the
access point closes on its own.
Where to get the token:
- Download the LM Token app for your OS from the Releases
page, unzip, and double-click it. Sign in with your La Marzocco account, pick
your machine, and hit Copy token. This is the only step that uses the
internet, and it runs on your computer. (Prefer a terminal? The
lmtokenCLI is on the same page.)
Prefer to build LM Token /
lmtokenfrom source (Go), or script it? Seetools/lmtoken/README.md.
On the same setup page you can enter your home Wi‑Fi name and password. The device then joins your network, gets an IP, and syncs the clock over NTP. Pick your city under Settings → Apollo → WiFi → Timezone. Auto‑sync can be turned off there too (Auto time (NTP)).
Because the setup page is always reachable from Set up WiFi, you can never be locked out if your network changes.
- Home shows the machine (and scale, if paired). The large action button is Standby / Turn On when connected, and becomes a Connect button when the machine is disconnected. With a scale, the pill under the shot timer cycles the shot mode — Auto shot (wired paddle boards) / Shot detect / Manual — and becomes Reset while a finished shot is up for review.
- Settings groups everything under Micra, Scale, and Device (brightness, clock, units, theme, Wi‑Fi).
- Stats shows brew/boiler temperature history, the shot History log (SD‑card boards), and device info.
Every screen and setting is described in the user manual — it opens with a map of every screen and setting if you're looking for where something lives.
The firmware targets Waveshare ESP32 touch boards. One board is selected at build time.
Four boards are supported, and each has its own firmware image. Every one of them delivers the full brew‑by‑weight experience with zero wiring (via Shot detect); the wiring column only matters if you also want Auto shot — the wired‑paddle mode where the machine's own paddle starts the shot and the firmware cuts it at target weight.
| Pick | Best for | Screen | Enclosure | Auto‑shot wiring (optional) | Performance |
|---|---|---|---|---|---|
| ESP32‑P4‑WIFI6‑Touch‑LCD‑5 (SKU 33762) | Mounting on the Micra | 5" 1280×720 | 3D‑printed shell | DIY cable with an external opto module | Best |
| ESP32‑P4‑WIFI6‑Touch‑LCD‑4.3 | Mounting on the Micra, smaller screen | 4.3" 800×480 | 3D‑printed shell | DIY cable with an external opto module | Best |
| ESP32‑S3‑Touch‑LCD‑4.3C BOX (SKU 33630) | Mounting on the Micra with nothing to build | 4.3" 800×480 | Finished box | Built‑in opto — three wires into screw terminals, nothing to build | Good |
| ESP32‑P4‑WIFI6‑Touch‑LCD‑X 8" box | Counter‑top companion | 8" 1280×800 | Finished box | DIY cable with an external opto module | Best |
Start with the ESP32‑P4‑WIFI6‑Touch‑LCD‑5. It's the recommended mount‑on‑the‑Micra option: the P4 is much faster than the S3 (32 MB flash + 32 MB PSRAM against 16/8), and the 5" panel is the right size on the machine. The cost is two bits of DIY — you print a shell, and if you want Auto shot you make up a cable with an external opto module.
The ESP32‑P4‑WIFI6‑Touch‑LCD‑4.3 is the same board with a smaller screen. Identical electronics — same processor, radio, audio, battery path, SD slot and paddle wiring; only the panel differs (4.3" 800×480 instead of 5" 1280×720). Pick between the two on screen size and a small price difference, nothing else. It takes its own printed shell and its own firmware image.
Choose the S3‑4.3C BOX if you'd rather not print a case or build a cable. It's the only small board that arrives as a finished box and has the opto‑isolators built in, so Auto shot is three wires into screw terminals with no cable to assemble — genuinely the least‑hacking route to a fully wired setup. The trade is speed: it's the slower part. (The wiring guide covers both styles.)
The X 8" box is the counter‑top companion — the same P4 electronics behind the biggest screen, in a finished enclosure, for sitting beside the machine rather than on it. Its panel and silicon are verified; the Apollo image itself hasn't been confirmed on one yet.
Power, battery and RTC notes live in docs/HARDWARE.md.
Ready‑to‑print 3MF files live in hardware/3d-prints/:
| File | What it is |
|---|---|
apollo2-stand.3mf |
Counter‑top stand. Mounts the S3‑4.3C‑BOX directly (it has the matching holes), and every shell below mounts to it the same way. |
apollo2-magnet-mount.3mf |
Optional magnet mount — attaches the device to the Micra's top corner instead of the counter. |
apollo2-wiring-gasket.3mf |
Optional wiring gasket — spaces the Micra's cover so the wiring can run underneath it. |
esp32-p4-5-shell.3mf |
Shell for the ESP32‑P4‑WIFI6‑Touch‑LCD‑5. |
esp32-p4-5-shell-slim.3mf |
Slimmer shell for the ESP32‑P4‑WIFI6‑Touch‑LCD‑5 — no battery compartment (USB‑power only). |
esp32-p4-4.3-shell.3mf |
Shell for the ESP32‑P4‑WIFI6‑Touch‑LCD‑4.3. |
esp32-p4-x-8-backplate.3mf |
Backplate that adapts the ESP32‑P4‑WIFI6‑Touch‑LCD‑X 8" box to the counter‑top stand above. |
This short video shows how the magnet mount and wiring gasket fit together on the machine.
Fasteners:
- Shells: 8 × M2.5×0.45, 5 mm screws each — 4 fasten the board into the shell, 4 fasten the shell cover.
- Stand mount: 2 × M4‑0.7×8 mm screws — same spec whether you're mounting the S3‑4.3C‑BOX or any of the printed shells.
- Magnet mount: 8 × 10 mm × 3 mm neodymium disc magnets.
The code is layered so the same UI runs on a real board and on a laptop:
include/core/ Pure interfaces (ports) + domain types. No LVGL, Arduino,
BLE, or SDL — just C++ and structs. e.g. IMachine, IScale,
IClock, INetwork, IProvisioner, IBrewController, IShotStore
(SD shot history), and the BLE central port (ble::ICentral).
src/core/ Portable protocol logic: the La Marzocco Micra link and
the Bookoo scale driver, written only against ble::ICentral
— so a new platform (Linux/BlueZ, Pico/btstack) reuses the
Bluetooth protocol code unchanged and implements only the
transport. Also the device-independent decisions: which
event makes which sound (sound.cpp), when a warm-up counts
as finished (ready_chime.h).
src/ui/ The LVGL user interface. Depends ONLY on core/ interfaces,
never on a concrete platform. Portable.
src/platform_esp32/ Device implementations of the core ports: the NimBLE GATT
transport, NVS config, display/touch drivers, Wi-Fi
station + NTP, the setup-portal web server.
src/platform_host/ "Fake" implementations that feed canned data, so the UI can
be built and rendered on a host with no hardware.
src/device/main.cpp Device entry: wires the real implementations to ui::App.
src/sim/main.cpp Simulator entry: wires the fakes, renders frames to PNG.
The UI is written against the core:: ports and is injected with concrete
implementations at startup (App::build(...)). Swapping the real BLE machine for
a FakeMachine is all that separates a board build from a laptop render — the UI
code is byte‑for‑byte identical.
No hardware needed. Builds a native executable that renders each screen/layout
to renders/*.png:
make sim # build + run, writes renders/*.pngThis is the fastest way to iterate on UI: change code, make sim, look at the
PNGs. Every supported screen size and several states are rendered.
The renders/ folder is git‑ignored build output; the screenshots in
docs/img/ are curated copies, and the annotated manual images in
docs/img/manual/ are generated from renders by make docs-img
(tools/annotate_docs.py + its manifest). When the UI changes, run
make sim, refresh the affected docs/img/*.png, and re-run make docs-img
as part of the same change so the README and manual stay accurate.
PlatformIO (pip install platformio) builds everything, and node (any
recent LTS — brew install node) builds the shot-history web page the device
serves. That page is embedded in the firmware as a generated header,
include/platform_esp32/webapp_dist.h, which is not committed: every
make build/make flash target rebuilds it when anything under
tools/webapp/ changes, and the release workflow does the same,
so a board can never serve a page that has drifted from the source. make sim
needs no node — the simulator has no web server.
pio run does not generate the web-app header (that dependency lives in the
Makefile), so run make webapp first — or just use the make targets, which
handle it.
pio run -e esp32-p4-micra-5 # 5" 1280x720 (P4, MIPI-DSI, WiFi6/BLE via C6)
pio run -e esp32-p4-micra-43 # 4.3" 800x480 (P4, MIPI-DSI, same board as the 5)
pio run -e esp32-s3-micra-4-3c # 4.3" 800x480 (S3, RGB panel)
pio run -e esp32-p4-micra-x-8 # X-series 8" box (P4, 1280x800)
pio run -e sim # native simulatorplatformio.ini also carries a few envs for boards that aren't released — they
build, and they're kept as the record of how those boards differ. make build-release compiles the three published images; make build-all compiles
every env.
Build environments and per‑board flags live in
platformio.ini; the pin/panel definitions for each board are
in include/platform_esp32/board_config.h.
Add an #elif defined(BOARD_...) block in board_config.h with the same
constant names the drivers read (pins, panel size, feature macros), then add a
matching [env:...] in platformio.ini with the -DBOARD_... flag. Driver code
never hardcodes a pin — it reads board:: constants — so a new board is mostly a
config block.
That makes the board buildable, not supported. Releasing one is a separate,
deliberate step: a row in the .github/workflows/firmware-release.yml matrix
(whose board value must equal the block's kUpdateSlug, since the
self‑updater fetches <site>/<tag>/firmware/app/<slug>.bin), a card in
site/index.html, and a row in the tables above. Releases are tagged vX.Y.Z;
a vX.Y.Z-beta.N tag publishes the same images as a pre-release, listed only in
releases-beta.json (the stable releases.json every device reads stays
releases-only) — promoting one to stable is a rebuild at the real tag, never a
copy of the artifacts, because fw::kVersion is compiled into the image.
Publishing is gated: the workflow builds unattended, but the jobs that create
the Release and push gh-pages (the OTA origin) run in a protected
environment that holds the only write credential and waits for the owner's
approval, and repository rulesets let only an admin touch gh-pages or a
v* tag. Several envs stay on the
buildable side on purpose.
include/core/ Domain interfaces + types
src/core/ Portable protocol implementations (Micra BLE, scales,
audio cues)
include/platform_esp32/ Device driver headers + board_config.h
include/platform_host/ Host fakes
include/ui/ UI headers (widgets, screen profiles, timezones)
include/vendor/ Vendored third-party headers (stb_image_write)
src/ Implementations (see Architecture above)
hardware/3d-prints/ Printable stand, board shells + mounts (3MF)
tools/ sim.sh, flash.sh, lmtoken (Go), PlatformIO helper scripts
renders/ Simulator output (PNG)
This project stands on the work of others. Grateful thanks to:
- pylamarzocco by Josef Zweck
(MIT) — the reference for La Marzocco's Bluetooth protocol (GATT
characteristic UUIDs, the JSON command/state payloads, machine name prefix) and
the cloud auth flow that
tools/lmtokenre‑implements in Go. - goscale (Apache‑2.0) — the model for the scale interface and the Bookoo Themis notification decode.
- apollo — brew‑by‑weight / paddle‑stop approach that inspires the (in‑progress) brew controller.
- Waveshare — board bring‑up details and register maps for the CH422G IO expander, GT911 / CST816 touch controllers, and the RGB panel timings, from their published ESP32‑S3 demos.
- stb_image_write by Sean Barrett (public
domain / MIT) — vendored in
include/vendor/for PNG output in the simulator; its license is retained in the file.
Library dependencies (fetched by PlatformIO): LVGL (MIT), NimBLE‑Arduino (Apache‑2.0), Arduino‑ESP32 (LGPL‑2.1 / Apache‑2.0), GFX Library for Arduino (BSD‑style), and ArduinoJson (MIT). Each retains its own license.
MIT © 2026 Marcus.
Not affiliated with or endorsed by La Marzocco. "La Marzocco" and "Micra" are trademarks of their respective owner; used here only to describe compatibility.




