A desk-side batch counter for 3D printing. An ESP32 with a 2.8" ILI9341 TFT shows how many parts are still missing from your print run — at a glance, from across the workshop. The count lives in Home Assistant, so it survives power loss and can be updated from your phone, an NFC tag, or automatically when your printer finishes a job.
Printing fifty of the same part and losing track of how many you already have? That was exactly the problem this solves.
The label, the caption and the batch size are all yours to set — this one is counting cones in German.
Everything you need is here: the ESPHome configuration, the Home Assistant helpers and automations, and STL files for a printed case — because a bare PCB taped to the workbench gets old fast.
┌──────────────────────────────────────────┐
│ P R I N T J O B S │ ← your own label
├──────────────────────────────────────────┤
│ │
│ 1 8 │ ← how many are LEFT
│ │
│ STILL TO PRINT │
│ │
│ ████████████████████░░░░░░░░░░░░░░░░ │ ← progress bar
│ │
│ 32 / 50 - 64% │
└──────────────────────────────────────────┘
320 × 240, landscape
When the batch is finished, the screen shows the total and ALL DONE!
Runs in greyscale on a plain ESP32 — a deliberate trade-off, explained under The memory problem. Swap in a WROVER module and it renders in full colour.
- Why the count lives in Home Assistant
- What you need
- The case
- Wiring
- Installation
- Counting up automatically
- Customising the display
- The memory problem
- Troubleshooting
The ESP32 stores nothing and decides nothing. It subscribes to one Home Assistant helper (input_number) over the encrypted ESPHome native API and renders it.
That is a deliberate design choice:
| Power-loss safe | Home Assistant persists the value. Unplug the ESP32, pull the printer's power — the number is still right. |
| Update from anywhere | Phone, dashboard, voice assistant, NFC tag, or an automation triggered by the printer itself. |
| No flash wear | No writes to ESP32 flash on every single count. |
| Trivially extendable | Notifications, statistics, history graphs — all the Home Assistant machinery works on the value for free. |
| Multiple displays | Flash the same config to a second ESP32 and both show the same number. |
If you'd rather not depend on Home Assistant, see Standalone variants.
| Part | Notes |
|---|---|
| ESP32 dev board | Any ESP32-WROOM board (DevKit v1, NodeMCU-32S, …). ~5 €. A WROVER with PSRAM (~7 €) buys you full colour instead of greyscale — see The memory problem. |
| 2.8" SPI TFT, 240×320, ILI9341 | Sold as KMRTM28028-SPI, MSP2807, or plain "2.8 inch SPI TFT ILI9341". ~8 € |
| Jumper wires | Female-female, 8 pieces |
| USB power supply | Any 5 V phone charger |
| Filament | ~40 g for the case, any material you like |
The touch controller (XPT2046) and the SD card slot on these modules are not used. Boards without touch work fine, and you do not need an SD card.
Electronics come to roughly 13 €.
Three printed parts hold the display and the ESP32 in a compact housing that stands on the bench or screws to a wall.
| File | Part |
|---|---|
case/front-bezel.stl |
Front frame — the display drops in from behind |
case/back-shell.stl |
Back shell holding the ESP32, with mounting tabs |
case/side-cap.stl |
Side cap with the USB cutout |
These are ordinary enclosure parts with no overhangs that need help — orient them as they come out of the slicer and they print without supports.
- Material: PLA or PETG. PETG if the display sits near the printer and might see some warmth.
- Layer height: 0.2 mm is plenty. The bezel looks a little crisper at 0.16 mm.
- Infill: 15–20 %
- Perimeters: 3, so the screw tabs on the back shell hold properly
The parts clip and screw together; the exploded view above shows the order of assembly. Print the bezel face-down for the cleanest visible surface.
Printed it and tweaked the settings? Pull requests with better print profiles are welcome — see Contributing.
Eight connections. Touch and SD stay unconnected.
| # | Display pin | → | ESP32 pin | Purpose |
|---|---|---|---|---|
| 1 | VCC |
→ | 5V (or VIN) |
Power |
| 2 | GND |
→ | GND | Ground |
| 3 | CS |
→ | GPIO5 | Chip select |
| 4 | RESET / RST |
→ | GPIO21 | Reset |
| 5 | DC / RS |
→ | GPIO22 | Data/command |
| 6 | SDI (MOSI) |
→ | GPIO23 | Data to display |
| 7 | SCK |
→ | GPIO18 | Clock |
| 8 | LED |
→ | 3V3 | Backlight, permanently on |
Leave unconnected: SDO/MISO and all five T_ pins.
A few things worth knowing before you plug anything in:
Power the ESP32 down while wiring. Plug everything, then switch on.
VCC goes to 5V, not 3V3. These modules carry their own 3.3 V regulator (the small component right next to the VCC pin). If yours genuinely doesn't have one, use 3V3 instead — 5 V without a regulator will damage the panel. Data lines are 3.3 V logic either way.
LED to 3V3, not to a GPIO. Some batches draw more current than a single ESP32 pin should source. Tied to 3V3 the backlight is simply always on, which is what you want here.
DC is the pin people get wrong. On many boards it is labelled RS. If the panel later stays white without a memory error in the log, check this line first.
Why GPIO 5 / 21 / 22? Many tutorials use GPIO 15 / 2 / 4 — all three are ESP32 strapping pins, and GPIO 2 and 15 in particular can stop the board from booting if a peripheral holds them at the wrong level at power-up. GPIO 21 and 22 are not strapping pins at all. GPIO 5 is one (it selects the SDIO timing) but must be high at boot, which is exactly the idle state of a chip-select line — that is why it is the conventional VSPI CS pin and works reliably here. ESPHome prints a generic warning about it; you can ignore that, or move pin_cs to GPIO 27 for a quiet log. Change pins at the top of print-counter.yaml, not in the display block.
Settings → Devices & Services → Helpers → Create helper → Number
| Field | Value |
|---|---|
| Name | Print counter |
| Minimum | 0 |
| Maximum | 50 (your batch size) |
| Step size | 1 |
| Display mode | Input field |
This creates input_number.print_counter. A YAML equivalent is in homeassistant/helpers.yaml.
One thing to get right: do not set an initial: value. With it, the count resets to zero on every Home Assistant restart. Without it, Home Assistant restores the last value — which is the whole point.
git clone https://github.com/LLAlexX78/esphome-print-counter.git
cd esphome-print-counter
cp secrets.yaml.example secrets.yamlEdit secrets.yaml with your WiFi credentials and an API encryption key (generate one at https://esphome.io/components/api.html).
Then adjust the substitutions: block at the top of print-counter.yaml:
substitutions:
title: "PRINT JOBS" # header text
caption: "STILL TO PRINT" # caption under the number
done_text: "ALL DONE!" # shown when the batch is complete
target: "50" # batch size
ha_entity: input_number.print_counter # your helperUsing the ESPHome CLI:
pip install esphome
esphome run print-counter.yamlOr with the ESPHome Device Builder add-on in Home Assistant: create a new device, paste the contents of print-counter.yaml into its editor, then Install → Manual download → Modern format. Flash the resulting .bin at https://web.esphome.io in Chrome or Edge (Firefox has no WebSerial).
The first build takes 5–15 minutes — ESPHome downloads the whole toolchain and the fonts. Every update after the first one goes over the air; you never need the USB cable again.
The device appears automatically under Settings → Devices & Services → ESPHome. Confirm, and the display starts showing the value within a second.
The helper can be incremented by anything Home Assistant can trigger on. Ready-made examples are in homeassistant/automation-examples.yaml:
- Klipper / Moonraker (Creality K2, Voron, …) — triggers on print state
complete - OctoPrint — triggers when the job ends at 100 %
- Bambu Lab — triggers on print status
finish - NFC tag — stick a tag next to the build plate, tap it when you remove the part
- Batch-complete notification
Or just tap +1 on your phone: homeassistant/dashboard-card.yaml is a ready Lovelace card with +1 / -1 / Reset buttons.
A trap worth avoiding: it is tempting to guard against double-counting by comparing the filename of the finished print with the last one counted. Don't. In a production run you print the same file fifty times — the names are identical, the condition never matches, and nothing gets counted after the first part. Guard on the previous state instead (only count if the printer was actually printing before), which also catches the restart case where the sensor jumps from unknown straight to complete.
Everything visual is in print-counter.yaml:
- Colours — the
color:block. Given as greyscale by default; the original colour values are noted in a comment for PSRAM boards. - Fonts — the
font:block. Fonts are downloaded from Google Fonts at compile time, so the machine running ESPHome needs internet once. To build fully offline, drop a.ttfnext to the YAML and usefile: "MyFont.ttf". - Layout — the
lambda:in thedisplay:block. Coordinates are plain pixels on a 320 × 240 canvas. - Portrait mode — set
rotation: 0and swap theW/Hconstants at the top of the lambda.
The glyphs: strings list every character the font can render — German umlauts are already included. If your header uses other characters (accents, Cyrillic, emoji), add them to glyphs: or compilation will fail with an "invalid glyph" error.
Got an ST7789 instead? Change model: ILI9341 to model: ST7789V in the display: block. The ili9xxx component supports a long list of controllers.
Worth understanding before you change anything about the display, because it shapes the whole configuration.
A 320 × 240 panel at 16 bits per pixel needs a 150 KB frame buffer, allocated in one contiguous block. A plain ESP32 has 320 KB of RAM in total; once the WiFi stack, lwIP, the encrypted API and ESPHome itself have taken their share, roughly 170 KB of fragmented heap remains. The allocation fails almost every time. The driver then reports:
[C][ili9xxx]: => Failed to init Memory: YES!
[E][component]: display is marked FAILED: unspecified
and the panel stays white — which looks exactly like a wiring fault, but isn't. If you take one thing from this README, let it be this: read the log before you touch a single cable.
This config solves it with color_palette: GRAYSCALE, halving the buffer to 77 KB, plus sram1_as_iram: true and no captive_portal: to free a little more. The result runs reliably on a bog-standard ESP32 — in greyscale. That is also why the colours are defined as true greyscale values: converted automatically, orange and green end up the same brightness and become indistinguishable.
Want full colour? Use an ESP32-WROVER module (~7 €, 4 MB PSRAM), then delete the color_palette: line and restore the colour values noted in the color: block. Nothing else changes.
The config also reports free heap to Home Assistant as a Free memory sensor. If it settles below ~20 KB, you are close to the edge.
Prefer no Home Assistant dependency? Two straightforward modifications:
- Physical button + local storage — replace the
homeassistantsensor with abinary_sensoron a GPIO and aglobals:value withrestore_value: true. The count then lives in ESP32 flash. - Touch buttons — the module's XPT2046 is supported by ESPHome's
xpt2046touchscreen component; wireT_CSto a free GPIO and addtouchscreen:plusbinary_sensortouch areas.
Pull requests adding these as separate example configs are welcome.
Read the logs first. ESPHome dashboard → your device → ⋮ → Logs. It works over WiFi, no USB needed, and it will usually tell you the answer outright.
| Symptom | Likely cause |
|---|---|
White screen, backlight on, log says Failed to init Memory / display is marked FAILED |
Not your wiring — out of RAM. See The memory problem. This is the single most common failure on a plain ESP32. |
| White / blank screen, no such log line | Wiring. Re-check CS, DC, RESET. DC is the one people mix up most — it is labelled RS on many boards. |
| No backlight at all | LED pin not connected to 3V3. |
| Colours inverted | Flip invert_colors: in the display: block to true. Since ESPHome 2026.7 this option is mandatory, so it is already present. |
| Mirrored or sideways | Change rotation: to 0, 180, or 270. |
| "Waiting for Home Assistant …" | The device isn't adopted yet, or ha_entity: doesn't match your helper. Check the exact ID under Developer tools → States. |
| Board won't boot after wiring | You used a strapping pin (GPIO 0, 2, 12, 15). Use the pinout in this README. |
| Compile error about glyphs | A character in your title: / caption: isn't in the glyphs: string. Add it. |
'invert_colors' is a required option |
ESPHome 2026.7 made it mandatory. Add invert_colors: false to the display: block. |
| Count resets on every HA restart | Your helper has an initial: value. Remove it. |
| Number stuck at maximum | The helper's max: is lower than your target:. They must match. |
This repository is deliberately free of anything environment-specific:
- No credentials, tokens, or API keys —
secrets.yamlis gitignored; onlysecrets.yaml.examplewith placeholders is tracked. - No Home Assistant URLs, internal hostnames, IP addresses, or network topology.
- No database files, logs, or
.storage/content. - No camera originals — phone photos carry EXIF metadata (device, timestamp, often GPS coordinates), so
*.heicand*.dngare gitignored. Export to JPG/PNG without metadata before adding images. - Entity IDs shown are generic defaults, not anyone's real setup.
If you fork this, keep it that way: never commit your own secrets.yaml. The .gitignore covers it, but check git status before your first push anyway.
Issues and pull requests are very welcome — especially:
- Print profiles and photos of your build
- Case variants: wall mount, magnetic back, different display sizes
- Configs for other display controllers or ESP32 variants
- Automation examples for more printer ecosystems
- Layout variants (multiple batches on one screen, ETA, filament estimate)
- Translations of this README
No contribution is too small. A typo fix is a contribution.
MIT — see LICENSE. Do whatever you like with it, including the case files.
ESPHome · ESP32 · ILI9341 · 2.8 inch TFT · SPI display · Home Assistant · 3D printing · batch counter · print counter · remaining prints · 3D printed case · STL · KMRTM28028 · MSP2807 · Klipper · Moonraker · OctoPrint · Creality K2 · Bambu Lab · smart home · DIY · maker

