Firmware project that turns a small ESP32‑S3 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.
- Micra control over Bluetooth (BLE) — brew temperature set‑point, steam boiler level + on/off, power / standby, and live status (connecting, ready, disconnected) with the real brew and steam temperatures. On boards with a speaker it chimes once when the machine finishes warming up — so you can start it and walk away — and stays quiet through the reheats that follow; the chime has its own volume setting (Off / 25 / 50 / 75 / 100 %).
- Bluetooth scale integration — pair a supported scale (Bookoo Themis, or an Acaia — Umbra, Lunar, Prochef, or Pyxis, the last untested) for a live weight readout, an automatic shot timer, a live flow‑rate graph (g/s or g), and tare from the screen.
- Brew by weight — with a scale paired, pick a shot mode from the Home screen: Auto shot (boards wired into the paddle circuit stop the shot at your target weight, learning the drip overshoot per shot), Shot detect (start/stop inferred from the weight stream alone — works on every board, no wiring), or Manual. Finished shots freeze into a review graph, and wired boards can auto‑flush the group after you lift the cup, flush it on demand from Home, and run a backflush cleaning cycle (10 × 4 s on / 4 s off) from Settings.
- Shot history on SD card (P4 boards and the S3 4.3C) —
every finished shot is recorded to a FAT‑formatted microSD card (any size —
a shot is a few tens of KB, so a small old card holds decades): stats and
the full weight/flow series as CSV under
/Apollo2/— a take‑away database you can read on any computer. The Stats tab's History section shows totals, lifetime/30‑day accuracy, and a filterable shot list; tap a shot for its full‑screen card. With WiFi on, the device also serves a web page at its local IP — browse and download your shots from a phone or computer, themed to match the device. - Automatic time — optionally join your home Wi‑Fi and the clock keeps itself correct over NTP, with a timezone picker that handles daylight saving. Time is saved to the on‑board RTC (where present) so it survives a power‑off.
- Phone‑based setup, no app — pairing and Wi‑Fi credentials are entered
through a tiny web page the device serves from its own Wi‑Fi access point; you
just join
Micra-Setupand open it in a browser. - Made to live on the counter — themes, °C/°F, 12/24‑hour clock, adjustable brightness, and a temperature‑history view. Layouts scale from a 2" pocket remote to a 7" panel.
Everything is designed to keep working if the machine, the scale, or Wi‑Fi is absent — the UI just shows the relevant part as offline.
The firmware targets Waveshare ESP32 touch boards. One board is selected at build time.
Two platforms are the focus going forward — pick by how hands‑on you want the build to be:
- Waveshare ESP32‑S3‑Touch‑LCD‑4.3C in the BOX variant (SKU 33630) — the easy path. A finished enclosure, ready to sit on the counter, and direct‑wire capable: its isolated DI/DO terminal block has the opto‑isolators built in, so wiring the Micra's paddle circuit for Auto shot is just three wires into screw terminals (see the wiring guide). The trade‑off is a lower‑end panel (4.3" 800×480 RGB) and less headroom than the P4.
- Waveshare ESP32‑P4‑WIFI6‑Touch‑LCD‑5 (SKU 33762) — the performance path. A much faster ESP32‑P4 with a crisp 5" 1280×720 DSI panel. More of a project: there is no enclosure (print the shell provided in this repo) and the wired paddle needs a small cable you assemble with an external opto‑isolator module (see the wiring guide).
| Board | Display | Wired paddle (Auto shot) | Notes |
|---|---|---|---|
| ESP32‑S3‑Touch‑LCD‑2 | 2.0" 240×320, ST7789 (SPI) | — (Shot detect only) | A portable, battery‑friendly remote. |
| ESP32‑S3‑Touch‑LCD‑4.3B | 4.3" 800×480, RGB parallel | — (Shot detect only) | Counter‑top panel. Has a PCF85063 RTC. |
| ESP32‑S3‑Touch‑LCD‑4.3C / 4.3C‑BOX | 4.3" 800×480, RGB parallel | Yes — built‑in. Isolated DI/DO screw terminals (opto‑isolators on board) | Recommended (easy path). Dimmable backlight, battery monitoring, PCF85063 RTC, speaker. |
| ESP32‑S3‑Touch‑LCD‑7B | 7" 1024×600, RGB parallel | — (Shot detect only) | Largest panel. |
| ESP32‑P4‑WIFI6‑Touch‑LCD‑4.3 | 4.3" 800×480, MIPI‑DSI (ST7701) | Yes — external opto. Native GPIOs + a PC817‑style opto module you wire | ESP32‑P4 (32 MB flash / 32 MB PSRAM); WiFi 6 + BLE via on‑board ESP32‑C6. |
| ESP32‑P4‑WIFI6‑Touch‑LCD‑5 | 5" 1280×720, MIPI‑DSI (HX8394) | Yes — external opto. Same wiring as the P4 4.3 | Recommended (performance path). Same electronics as the P4 4.3 with a higher‑density panel (UI scaled 1.5×). No enclosure — a printable shell is in the repo. |
Boards without paddle wiring still get the full brew‑by‑weight experience via Shot detect — only the automatic stop at target weight needs the wire. Step‑by‑step wiring instructions (with photos of the Micra's paddle loom) are in the wiring guide.
The S3 boards use the ESP32‑S3R8 (16 MB flash, 8 MB octal PSRAM). A supported Bluetooth scale (Bookoo Themis, or Acaia Umbra / Lunar / Prochef / Pyxis — Pyxis untested) is optional but unlocks the shot timer, flow graph, and brew‑by‑weight features.
The boards run from USB‑C power — that's the normal way to use them. An optional battery can be installed (the boards have a battery connector), but it only lasts a few hours, so treat it as a nice‑to‑have for moving the device around, not a way to run it. An optional RTC coin cell (boards with an RTC, e.g. the 4.3B/4.3C) keeps the clock through a power‑off — but it's only needed if you don't configure Wi‑Fi + NTP, which sets the time automatically on every boot.
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-s3-4.3c-shell.3mf |
Shell for the bare ESP32‑S3‑Touch‑LCD‑4.3C (no‑enclosure variant). |
esp32-p4-5-shell.3mf |
Shell for the ESP32‑P4‑WIFI6‑Touch‑LCD‑5. |
esp32-p4-4.3-shell.3mf |
Shell for the ESP32‑P4‑WIFI6‑Touch‑LCD‑4.3. |
esp32-s3-2-shell.3mf |
Shell for the pocket ESP32‑S3‑Touch‑LCD‑2. |
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.
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.
Building from source requires PlatformIO (pio) and
a USB cable.
make flash # print selection of flash options
make flash-s3-4-3b # or target a board: s3-2 | s3-7b | s3-4-3b | s3-4-3c | p4-4-3 | p4-5
make monitor # open the serial console (115200 baud)Settings → Micra → Bluetooth → Scan, then pick your machine.
- If it has never been paired to the La Marzocco app, the machine still has its factory default token and the device reads it over Bluetooth automatically — you're connected, nothing to type.
- If it's already paired to the LM app, that default token has been rotated, so the auto‑read can't get it and you'll be prompted to enter the current one — see step 3.
Tap WiFi on the prompt (or Settings → Micra → Set up) to start the
device's own Wi‑Fi access point, Micra-Setup. Join it from your phone, 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:
- Machine already paired to the LM app → 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.) - Machine never paired to the LM app → its default token is also printed as a QR code inside the machine, if you'd rather scan and paste it than let step 2 read it automatically.
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 → Device → 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.
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 README screenshots in
docs/img/ are curated copies. When the UI changes, run make sim and refresh
the affected docs/img/*.png as part of the same change so the README stays
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-s3-micra # 2-inch firmware (default)
pio run -e esp32-s3-micra-4-3b # 4.3" 800x480 (S3, RGB panel)
pio run -e esp32-s3-micra-4-3c # 4.3" 800x480 (S3, RGB panel, dimmable + battery)
pio run -e esp32-s3-micra-7b # 7" 1024x600
pio run -e esp32-p4-micra-43 # 4.3" 800x480 (P4, MIPI-DSI, WiFi6/BLE via C6)
pio run -e sim # native simulatorBuild 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.
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.




