Hey there! ๐ PrintOrb turns a tiny round Waveshare ESP32-S3-Touch-LCD-1.28 into a live 3D-printer status orb that sits on your desk. It talks to either Klipper (Moonraker) or Bambu Lab and shows you progress, temps, remaining time, layers and โ for Bambu โ the full AMS filament state. ๐จ๏ธโจ
The best part? No recompiling to switch printers. Everything is configured at runtime through a built-in web portal, and the round screen is a swipeable, touch-driven carousel. Flash it once, set it up from your browser, done. ๐
Tip
๐งฉ No Home Assistant. No broker. No cloud. No companion server. PrintOrb is a fully standalone device that talks directly to your printer โ Bambu Lab over local MQTT (LAN mode) or Klipper via the Moonraker API. The only things between the orb and your printer are your WiFi and an access code. That's the whole point. โจ
Tip
Don't want to install a toolchain? Jump straight to Flash from the browser โ plug in the board, click one button, and you're running. โก
Glad you asked! Here's the good stuff:
- ๐งฉ Fully standalone โ no Home Assistant required: the orb connects directly to your printer over your LAN. No middleware hub, no MQTT broker, no cloud account, no companion server to keep running.
- ๐ Two backends, switchable at runtime: pick Klipper (HTTP polling of the Moonraker API) or Bambu Lab (local MQTT over TLS, port 8883, LAN mode) โ no reflash to swap.
- ๐ Touch carousel โ swipe left/right between five screens:
- ๐ Status โ progress ring, temps, time, layers (with crisp MDI icons)
- ๐ Details โ state, file, ETA (minimal & airy)
- โ๏ธ System โ WiFi/RSSI, IP, brightness (just tap โ/+)
- ๐งต Filament (Bambu/AMS) โ colored slot tiles, active slot, humidity; swipe up/down to switch AMS units (with a vertical dot indicator)
- ๐๏ธ Control โ Pause / Resume / hold-to-Stop
- ๐ Full Bambu AMS view: per-slot filament type, color and remaining %, active tray highlight, humidity, and multiple AMS units
- ๐ Web portal (baked into flash): live status, all settings, and a live device log for browser debugging
- ๐ถ First-time setup made easy: a captive-portal AP (
printorb-setup-xxxx) with a WiFi network scan and printer mDNS discovery - ๐ท๏ธ mDNS: reachable as
printorb.local; the printer address accepts an IP or a hostname (.local), and the hostname is configurable - ๐พ Persistence in NVS: survives reboots and re-flashes
- ๐ Scheduled dimming & power-save: time-aware backlight dimming and idle sleep (NTP-synced), so your orb isn't blazing at 3 AM
- ๐ OTA updates: flash new firmware over WiFi (ArduinoOTA) or right from the browser Update tab โ first flash is USB, after that you're wireless
Note
The AMS screen only appears when the printer type is Bambu โ the carousel adapts itself to your backend at boot.
Waveshare ESP32-S3-Touch-LCD-1.28
- ๐ง ESP32-S3R2 (2 MB QSPI PSRAM)
- ๐ข 1.28โณ round LCD, 240ร240, GC9A01 (SPI)
- ๐ CST816S capacitive touch (IยฒC)
| Function | GPIO |
|---|---|
| LCD SCLK | 10 |
| LCD MOSI | 11 |
| LCD MISO | 12 |
| LCD DC | 8 |
| LCD CS | 9 |
| LCD RST | 14 |
| LCD Backlight | 2 |
| Touch SDA | 6 |
| Touch SCL | 7 |
| Touch INT | 5 |
| Touch RST | 13 |
Warning
Backlight is GPIO 2 on this board revision. Some other Waveshare revisions
use GPIO 40 โ if the screen stays dark, that's the very first thing to check
(ORB_PIN_LCD_BL in include/lgfx_device.h, where all the pins live). ๐ฆ
You'll need PlatformIO (CLI or the VS Code extension). Then it's three commands:
# in the project folder
pio run # compile
pio run -t upload # flash (connect the board via USB-C)
pio device monitor # serial output (115200 baud)The web UI is embedded in flash โ no separate LittleFS upload needed. ๐
Every push to main builds the firmware and publishes a web flasher to GitHub
Pages: disane87.github.io/printorb.
Open it in Chrome / Edge / Opera on desktop, plug the board in via USB-C and click Connect & Install โ it flashes the merged image over Web Serial, zero PlatformIO required. The same merged binary is attached to every GitHub Release. ๐
Note
Serial routing: the USB-C port goes through the on-board CH343 UART
bridge, so Serial is routed to UART0 (ARDUINO_USB_CDC_ON_BOOT=0 in
platformio.ini). The serial monitor and the web log show the same output.
Important
Flash size: the partition table only uses the first 4 MB and works on both
4 MB and 16 MB units. On a flash-size mismatch during upload, set
board_upload.flash_size in platformio.ini to match your unit.
Ready to roll? Here's the whole flow:
- Flash the firmware and power up the board.
- On first boot (no WiFi saved) PrintOrb opens a captive-portal access point:
printorb-setup-xxxx(open network). Connecting with a phone should pop up the "sign in to network" page automatically โ otherwise just openhttp://192.168.4.1. - In the Settings tab:
- ๐ถ Scan for your WiFi, pick the network, enter the password
- ๐ท๏ธ Optionally set a hostname (default
printorbโprintorb.local) - ๐จ๏ธ Choose your printer type:
- Klipper: IP/hostname, Moonraker port (default
7125), API key optional - Bambu Lab: IP/hostname, serial number, LAN access code
- Klipper: IP/hostname, Moonraker port (default
- ๐ Or use Discover (mDNS) to find a Klipper/Bambu printer on your LAN
- ๐พ Save โ the device reboots and connects.
- After that the UI lives at
http://<device-ip>/orhttp://printorb.local/. ๐
- โ Enable LAN mode on the printer; the serial number and access code are shown right there.
- โ Works with P1/X1/A1/H2 over the local network for status + AMS. (Live camera is not supported on-device โ see Known limitations.)
One responsibility per file โ easy to find your way around:
printorb/
โโ platformio.ini Build configuration & libraries
โโ partitions.csv Partition table (4/16 MB)
โโ include/
โ โโ lv_conf.h LVGL configuration
โ โโ lgfx_device.h LovyanGFX panel/touch definition (PINS HERE)
โโ src/
โโ main.cpp Setup/loop, boot sequence, wiring
โโ config.{h,cpp} Settings + NVS persistence
โโ display.{h,cpp} LVGL bring-up (flush + touch)
โโ ui.{h,cpp} LVGL screens: boot, setup, and the touch carousel
โโ orb_icons.{h,c} Embedded Material Design icon font (nozzle/bed/โฆ)
โโ printer.h Shared status model (+ AMS) + client interface
โโ klipper_client.{h,cpp} Moonraker HTTP client (+ pause/resume/stop)
โโ bambu_client.{h,cpp} Bambu MQTT/TLS client (+ AMS, controls)
โโ wifi_manager.{h,cpp} STA + captive AP, DNS, mDNS, host resolution
โโ timekeeper.{h,cpp} SNTP local time for scheduled dimming
โโ ota.{h,cpp} ArduinoOTA (espota) WiFi flashing
โโ web_portal.{h,cpp} Async web server (API + scan/discover/log/update)
โโ web_index.h Embedded config UI (HTML/CSS/JS)
โโ logbuf.{h,cpp} In-memory log ring buffer (served at /api/log)
Tip
Adding a new printer backend? Implement the PrinterClient interface and
instantiate it in createPrinter() โ the shared shape is PrinterStatus, so
the UI and web portal pick it up for free. ๐
| Method | Path | Purpose |
|---|---|---|
| GET | / |
Config/status UI |
| GET | /api/status |
Current printer status (JSON) |
| GET | /api/config |
Current settings (without WiFi password) |
| POST | /api/config |
Save settings โ reboot |
| POST | /api/restart |
Restart the device |
| GET | /api/scan |
Async WiFi network scan (JSON) |
| GET | /api/discover |
mDNS discovery of Klipper/Bambu printers |
| GET | /api/sysinfo |
Diagnostics (IP, memory, flash, chip, uptime, NTP) |
| GET | /api/log |
Live device log (plain text) |
| POST | /api/update |
Firmware upload (raw .bin, HTTP Basic auth) โ reboot |
Once your orb is on the network, you don't need the USB cable anymore. ๐
OTA requires an Update / OTA password set in the web UI (Settings โ Security).
With no password, OTA is disabled as a secure default (Ota::begin() returns
early and /api/update always 401s). The first flash is still USB.
- ๐ป Dev push:
pio run -e esp32-s3-touch-lcd-128-ota -t upload(ArduinoOTA /espota; set--authinplatformio.inito the device password). Uses a deviceโhost reverse TCP connection โ a host firewall or crossing subnets breaks it, so use the browser path instead. - ๐ Browser: the Update tab uploads the raw
.bintoPOST /api/updatewith HTTP Basic auth (useradmin, the OTA password). The raw body (not multipart) keeps memory low, so big uploads don't reset the device while the Bambu MQTT buffer is live.
- ๐ท Camera: not available on-device. The Bambu H2 series streams H.264 over RTSPS (port 322), which the ESP32-S3 simply cannot decode. The simple JPEG chamber protocol (port 6000) only exists on X1/P1/A1. A live view would need an external transcoding proxy (e.g. go2rtc/ffmpeg โ JPEG snapshots).
- ๐ฆ Bambu MQTT buffer: the initial
pushallreport (with AMS) is large โ ~32 KB observed โ so the PubSubClient buffer is set to 48 KB inbambu_client.cpp. Too small a buffer makes PubSubClient silently drop the report and the status stays stuck on "Offline". - ๐ TLS unvalidated: the Bambu LAN broker uses a self-signed certificate; the
client connects with
setInsecure()(common on the local network). - ๐งฑ Klipper layers:
current/total layeronly appear if your slicer writes them toprint_stats.info(e.g. viaSET_PRINT_STATS_INFO). - ๐ mDNS discovery only works in STA mode (on your LAN). In first-time AP setup, enter the IP/hostname manually; resolution then works after reboot.
- ๐ถ AP mode is an open network purely for easy setup and is not active after the initial WiFi configuration.
Caution
The web portal is intentionally unauthenticated on a trusted LAN, and
/api/update accepts a firmware upload โ keep your orb on a network you trust,
and set the OTA password to guard the update endpoint.
Need a clean slate? Call Config::reset() (e.g. temporarily in setup()), flash,
boot once, then remove it again. The device starts back up in the setup AP, ready
for a fresh config. ๐งน
Versioning is fully automated with
semantic-release, driven by
Conventional Commits. On every push to
main, CI (.github/workflows/release-and-deploy.yml) builds the firmware, then
semantic-release figures out the next version from your commit messages, writes
CHANGELOG.md, tags the repo, and creates a GitHub Release with the firmware
attached โ and the web flasher on GitHub Pages is redeployed at that version. ๐
Commit prefixes that drive the version bump:
| Prefix | Example | Release |
|---|---|---|
fix: |
fix: correct AMS slot color mapping |
patch (x.y.Z) |
feat: |
feat: add scheduled display dimming |
minor (x.Y.0) |
feat!: / BREAKING CHANGE: footer |
feat!: drop legacy config keys |
major (X.0.0) |
chore: / docs: / refactor: / ci: โฆ |
docs: update wiring notes |
no release |
So: phrase commits as type: summary and the changelog + releases take care of
themselves. No conventional commits since the last tag โ no new release (the Pages
site just redeploys at the current version).
Building your printer stack? Check out these other projects from the same workshop:
| Project | Description |
|---|---|
| ๐ฏ Spoolman Home Assistant | Bring your Spoolman filament inventory into Home Assistant โ 25+ sensors per spool, run-out predictions and low-filament alerts. |
| ๐งต Spoolman MCP | MCP Server for Spoolman โ manage your filament inventory through AI assistants like Claude. Available on npm. |
| ๐ Klipper Sphere | Sibling project exploring Klipper status on round displays. |
| ๐ FilaPulse / Open Maker Database | Community database & tooling for the maker ecosystem. |
Want to make PrintOrb even better? Awesome โ PRs and issues are very welcome! ๐
A few things worth knowing:
- ๐พ Use conventional commits so
semantic-release can do its thing (
feat:,fix:,feat!:,chore:, โฆ). - โ
Anything touching
src/orinclude/must still passpio runโ the current baseline builds clean (โ44 % RAM, โ44 % of the 3 MB app partition). - ๐ฌ๐ง Comments, identifiers, docs and on-screen strings are all in English.
- โ๏ธ Keep settings in
OrbConfig/NVS โ no hardcoded IPs, SSIDs or codes.
Curious how the project came to be? The original request and design decisions live
in docs/CONVERSATION.md. ๐
Thanks for checking out PrintOrb! If your printer now has a glowing little orb keeping an eye on it, give the repo a โญ on GitHub โ it really helps! ๐
Found a bug? Got an idea? Open an issue and let's make it better together! ๐