A web frontend + backend service for Elegoo Centauri Carbon 2 (CC2) FDM printers. The Node.js service maintains a single MQTT connection to the printer and exposes state to browsers via WebSocket, REST API, and Prometheus metrics.
- Two-panel dashboard: Resizable sidebar + main grid, collapsible/reorderable cards, responsive breakpoints
- Print status: Progress with thumbnail, temperatures, fan speeds, toolhead position, active filament
- 3D gcode preview: Three.js toolpath visualization with layer follow mode and nozzle tracking
- Live charts: Temperature, fan speed, print speed, AI confidence, and layer time graphs with zoom/pan
- Canvas/AMS display: Spool-style filament slots with colors, types, filament editing, load/unload
- Camera feed: Live MJPEG stream with single-upstream fan-out proxy, snapshot button, fullscreen overlay
- Printer control: Temperature presets, fans, speed mode, LED toggle, XY/Z movement, emergency stop
- Print management: File browser with thumbnails/popovers, start dialog, pause/resume/stop, USB support
- Zone detection: Server-side toolhead zone tracking (print area, cutter, purge) for AI/event suppression
- AI print monitoring: Motion-based stall detection, SigLIP zero-shot classification, VLM analysis, zone-aware suppression, customizable labels
- Telegram notifications: Print events, progress updates, camera snapshots, AI alerts, firmware-update progress (one "do not power off" message when the printer starts an OTA flash, one when it completes or fails)
- MQTT Log: Real-time structured log with diff view, method filtering, pinning
- Debug panel: Live state tree with change tracking, watched paths, export
- Event log: Print events, errors, milestones with timestamps and severity
- Print history: Method 1036 history with auto-load on connect
- Print reports: PDF generation with stats, charts, snapshots
- Timelapse viewer: Download/play timelapse videos
- Spool calculator: Remaining weight/meters from measured thickness
- Moonraker/OctoPrint compatibility: API layers for Mainsail/Fluidd/KlipperScreen and OctoPrint clients
- MCP server: Model Context Protocol for AI agent integration — 6 resources, 31 tools
- Prometheus metrics:
/api/metrics/prometheusendpoint for monitoring - PWA support: Installable app with manifest + service worker
- Dark theme: Modern UI with CSS custom properties, responsive at 1200/800/480px breakpoints
The Node.js backend service (src/server/) connects to the printer's MQTT broker over TCP:1883 and acts as a bridge:
- WebSocket (
/ws): Real-time state updates pushed to all connected browsers - REST API (
/api/*): Snapshots, file operations, camera proxy, commands - Static files: Serves the built
dist/frontend in production (SPA fallback toindex.html) - Prometheus (
/api/metrics): Printer telemetry for monitoring
The CC2 printer runs its own MQTT broker on two ports:
- Port 1883 — MQTT over TCP (used by the service)
- Port 9001 — MQTT over WebSocket (legacy direct-connect mode)
Communication uses the CC2 MQTT protocol:
- Discovery: UDP broadcast on port 52700 (not available from browser — IP entered manually)
- Connect: MQTT 3.1.1 over WebSocket, auth
elegoo/123456(or access code) - Register: Publish to
elegoo/<sn>/api_register - Subscribe:
elegoo/<sn>/api_statusfor delta status updates - Commands: Publish to
elegoo/<sn>/<client_id>/api_request - Heartbeat: PING every 10 seconds to maintain connection
See CC2 Protocol Documentation for the full protocol reference.
docker run -d \
--name elegoo-web \
--restart unless-stopped \
-p 8088:8088 \
-p 7125:7125 \
-e PRINTER_IP=192.168.1.150 \
-v elegoo-data:/app/data \
ghcr.io/runnane/elegoo-web:latestWeb UI: http://localhost:8088 · Moonraker API: http://localhost:7125
To keep the data on a host path you can browse, instead of a named volume:
mkdir -p ./elegoo-data
docker run -d \
--name elegoo-web \
--restart unless-stopped \
-p 8088:8088 \
-p 7125:7125 \
-e PRINTER_IP=192.168.1.150 \
-v ./elegoo-data:/app/data \
ghcr.io/runnane/elegoo-web:latestEverything then appears under ./elegoo-data — reports/, gcode-cache/, logs/,
state.json, moonraker-db.json.
Mount the directory, not the individual files. A bind mount whose source does not exist yet is created by Docker as a directory, so
-v ./elegoo-data/state.json: /app/data/state.jsongives the service a directory where it expects a file. It does not crash — the container staysUpand the UI works — it just logsEISDIR: illegal operation on a directoryand silently never persists anything.
Copy the example file and edit your printer IP:
cp docker-compose.example.yml docker-compose.yml
# Edit PRINTER_IP in docker-compose.yml
docker compose up -dSee docker-compose.example.yml for all available environment variables (Telegram, AI monitoring, camera, etc.).
| Tag | What it is |
|---|---|
latest |
the newest build of main — use this unless you have a reason not to |
x.y.z, x.y |
a specific release, pinned |
Images are built for linux/amd64 and linux/arm64, so a Raspberry Pi next to the printer works. Each image carries its own build stamp — the version shows in the web UI's status dropdown, which is the first thing to quote when reporting a problem.
docker build -t ghcr.io/runnane/elegoo-web:local .
docker run -d -p 8088:8088 -p 7125:7125 -e PRINTER_IP=192.168.1.150 ghcr.io/runnane/elegoo-web:localA locally built image reports its version as unknown, which is expected: the stamp is
supplied by the publish workflow, not by docker build.
Mount a volume at /app/.cache as well. The CLIP model cache lives there, not under
/app/data, because transformers.js resolves its cache path relative to the working
directory. Without that volume the model is re-downloaded every time the container is
recreated — about 200 MB and roughly 95 seconds here, and a good deal slower on a Pi.
-v elegoo-model-cache:/app/.cache| Variable | Default | Description |
|---|---|---|
PRINTER_IP |
required | Printer IPv4 address. The service refuses to start without it |
PRINTER_PASSWORD |
123456 |
Printer access code |
PRINTER_SN |
— (discovered) | Printer serial number, e.g. F01U3UD3798YT8K. Normally discovered automatically and then cached in <DATA_DIR>/printer-sn.json, so this is rarely needed. Set it if a first start hangs at "registering": the printer only publishes while a client is registered, so a service that has never learned the serial has nothing to overhear |
SERVICE_PORT |
8088 |
Web UI / API / WebSocket port |
MOONRAKER_PORT |
7125 |
Moonraker compatibility API port |
BIND_ADDRESS |
0.0.0.0 |
Interface both HTTP servers listen on. IPv4 only — see .agents/security.md before narrowing it in production |
CAMERA_ENABLED |
true |
Enable camera MJPEG proxy |
CAMERA_URL |
http://<PRINTER_IP>:8080 |
Override camera URL. Applies to the PRINTER_IP printer only; a saved connection preset uses http://<its address>:8080 |
CONNECTION_PRESETS_API |
false |
Allow adding, removing and switching saved printer connection presets from Settings (/api/printers). Off by default because the API has no authentication: with it on, anyone who can reach the service can re-point it at another address. Listing presets always works. PRINTER_IP is always the built-in default preset, and PRINTER_PASSWORD is only ever sent to that printer |
CORS_ALLOWED_ORIGINS |
— (same-origin) | Comma-separated origins allowed to make cross-origin requests to /api/*, /mcp, /moonraker/*, /octoprint/* and :7125. Unset means no cross-origin access. * restores the old allow-everything behaviour |
TELEGRAM_BOT_TOKEN |
— | Telegram bot token (enables notifications) |
TELEGRAM_CHAT_ID |
— | Telegram chat ID — where notifications are sent |
TELEGRAM_ALLOWED_CHAT_IDS |
TELEGRAM_CHAT_ID |
Comma-separated numeric sender ids permitted to issue bot commands. Anyone else is ignored silently |
PROGRESS_INTERVAL |
25 |
Notify every N% progress |
DATA_DIR |
./data |
Data directory for state, reports, logs |
AI_ENABLED |
false |
Enable AI print monitoring |
AI_VLM_ENABLED |
false |
Enable VLM analysis. Opt-in: AI_ENABLED alone does not turn this on |
AI_VLM_PROVIDER |
ollama |
VLM provider: ollama or openai |
AI_VLM_API_KEY |
— | API key for OpenAI VLM provider |
AI_VLM_BASE_URL |
http://localhost:11434 |
VLM API endpoint (ollama's default port) |
AI_VLM_MODEL |
llava |
VLM model name |
AI_LOCAL_ENABLED |
true |
Enable local SigLIP zero-shot classification |
AI_LOCAL_MODEL |
Xenova/siglip-base-patch16-224 |
Local classification model |
AI_INTERVAL |
60 |
Seconds between AI analysis |
AI_ALERT_THRESHOLD |
3 |
Consecutive alerts before notification |
AI_ALERT_COOLDOWN |
300 |
Seconds between alert notifications |
All persistent data lives under /app/data inside the container:
| Path | Contents |
|---|---|
/app/data/state.json |
Persisted printer state (survives restarts) |
/app/data/connection-presets.json |
Saved printer connection presets and which one is active, including any per-printer access codes (written 0600) |
/app/data/moonraker-db.json |
Moonraker compatibility database |
/app/data/reports/ |
Print reports with snapshots and PDFs |
/app/data/gcode-cache/ |
Downloaded gcode files for 3D preview |
/app/data/logs/ |
MQTT capture logs (from debug panel) |
| Port | Protocol | Purpose |
|---|---|---|
| 8088 | HTTP/WS | Web UI, REST API, WebSocket, camera proxy, MCP |
| 7125 | HTTP/WS | Moonraker compatibility API (for Mainsail/Fluidd/KlipperScreen) |
- Node.js 22.13+
- pnpm 11. The exact version is pinned by
packageManagerinpackage.json, and an olderpnpmswitches to it by itself. - An Elegoo CC2 printer on the same network, set to LAN-only mode
pnpm install
pnpm devThis starts both the backend service and Vite dev server. Open http://localhost:5173.
The dev server answers to localhost only. To reach it by another name — a machine
hostname, a LAN address, a tunnel — list them in .env (which is gitignored), so that
no environment-specific hostname is committed:
VITE_ALLOWED_HOSTS=my-box.example.internal,10.0.0.5This is a dev-server setting: vite build ignores it, and production never runs vite.
pnpm buildProduction output goes to dist/. The service serves it automatically on port 8088.
Releases are cut by the Release workflow (Actions → Release → Run workflow, or
gh workflow run release.yml -f increment=auto). It runs the gates, bumps the version,
writes CHANGELOG.md from the conventional commit subjects since the last tag, tags,
publishes the GitHub release, and then builds the ghcr.io image for that tag. Tick
dry_run to preview the version and changelog without writing anything.
Every pull request is checked for a conventional subject (feat(ui): …, fix: …,
chore(deps): …) so that nothing merges that the changelog would then leave out.
Install as a systemd service:
pnpm build
sudo bash contrib/install.shThis creates:
- Service user
elegooweb - Installation at
/opt/elegooweb/ - systemd unit
elegooweb.service(auto-start on boot) - Default
.envconfig at/opt/elegooweb/.env
Edit /opt/elegooweb/.env to configure printer IP, Telegram, AI monitoring, etc.
sudo systemctl status elegooweb # Check status
sudo journalctl -u elegooweb -f # Tail logs
sudo systemctl restart elegooweb # Restart after config changes
sudo bash contrib/uninstall.sh # UninstallWeb UI: http://<host>:8088
src/
├── main.ts # Entry point, WsClient, render loop, sidebar resize
├── ws-client.ts # WebSocket client (connects to service, not printer)
├── types.ts # CC2 protocol types, status codes, zone detection
├── printer-state.ts # Browser-side state with delta merge + zones
├── log-store.ts # Ring buffer (500 entries) for MQTT log
├── chart-store.ts # Ring-buffer time-series store for charts
├── server/
│ ├── index.ts # Service entry point
│ ├── mqtt-bridge.ts # Singleton MQTT connection to printer
│ ├── state-store.ts # Centralized state, event detection, zone tracking
│ ├── ws-transport.ts # WebSocket server for browsers
│ ├── rest-api.ts # REST API, MJPEG fan-out proxy, Prometheus
│ ├── config.ts # Environment-based configuration (.env)
│ ├── logger.ts # Winston structured logging with rotation
│ ├── telegram.ts # Telegram bot notifications
│ ├── allowlist.ts # Who may talk to the Telegram bot
│ ├── ai-monitor.ts # AI print monitoring (SigLIP + VLM + motion)
│ ├── moonraker-compat.ts # Moonraker API compatibility
│ ├── moonraker-server.ts # Moonraker standalone server (:7125)
│ ├── octoprint-compat.ts # OctoPrint API compatibility
│ ├── mcp-server.ts # Model Context Protocol server
│ ├── state-persistence.ts # Persist/restore state across restarts
│ ├── print-report-collector.ts # Collect print data for reports
│ └── print-report-pdf.ts # PDF report generation
├── ui/
│ ├── dashboard.ts # Re-export barrel for all UI modules
│ ├── helpers.ts # Shared DOM/formatting utilities
│ ├── print-status.ts # Print status sidebar card
│ ├── service-status.ts # Header badge + dropdown (service health + system info)
│ ├── canvas.ts # Canvas/AMS spool visualization
│ ├── files.ts # File browser with popovers
│ ├── controls.ts # Control event handlers
│ ├── charts.ts # Canvas 2D live charts with zoom/pan
│ ├── gcode-preview.ts # 3D gcode toolpath (Three.js)
│ ├── log.ts # MQTT log panel
│ ├── log-methods.ts # MQTT method ID labels and filtering
│ ├── structured-log.ts # Structured log with diff/pin/filter
│ ├── system-info.ts # System information display component
│ ├── debug-panel.ts # Live state tree, change tracking, export
│ ├── settings.ts # Card layout + tab management
│ ├── event-log.ts # Print event log
│ ├── ai-panel.ts # AI monitor panel
│ ├── print-history.ts # Print history
│ ├── print-reports.ts # PDF print reports
│ ├── print-dialog.ts # Print start confirmation dialog
│ ├── maintenance.ts # Self-check, auto-level, vibration, PID
│ ├── timelapse.ts # Timelapse viewer
│ ├── layer-chart.ts # Layer time chart
│ ├── filament-editor.ts # Canvas filament editor
│ ├── spool-calc.ts # Spool calculator
│ ├── toast.ts # Toast notifications
│ ├── help.ts # Help tab / API docs
│ └── ui-settings.ts # UI preference persistence
└── styles/
└── main.css # Dark theme, two-panel layout, responsive
- Elegoo Centauri Carbon 2
- Other CC2-protocol printers (Elegoo Cura, etc.)
Resin printers (Mars, Saturn) use a different protocol (SDCP over WebSocket) and are not currently supported.
- Max 2 MQTT connections: The printer limits concurrent MQTT clients. The service uses one slot.
- No UDP discovery: Browsers can't send UDP — printer IP must be configured in
.env. - Camera CORS: The MJPEG stream on port 8080 is proxied through the service to avoid CORS issues.
- LAN-only: Cloud mode is not supported.
- Method 1045 (thumbnail) requires
file_name(with underscore), but 1046 (file detail) requiresfilename(no underscore). Using the wrong form returns error 1003. total_layeris often missing from delta status updates — fetched separately via method 1046.- Fan speed is PWM 0-255, not percentage. Convert:
pct = Math.round(speed / 255 * 100). gcode_move(notgcode_move_inf) — code normalizes the old name at ingest for firmware compat.- Sub-status 1066 is undocumented but observed during Canvas filament swaps (firmware 01.03.01.89).
- Canvas filament swaps: sub_status mostly stays at 2075 (Printing) with brief flickers to 1045/1066;
zones.currentis the reliable indicator (toolhead moves to cutter/purge areas). - Sensor-based filament runout (
filament_detected1→0) duringmachineStatus === 2always means filament change, never actual runout. Real runouts trigger exception codes 109/1211.
Server-side toolhead zone tracking based on gcode_move.x/y coordinates:
| Zone | Center | Boundary | Purpose |
|---|---|---|---|
cutter_area |
X=254, Y≈3.5 | X:245-265, Y:-5-15 | Filament cutter |
purge_area |
X=52.5, Y=264 | X:40-65, Y:257-275 | Purge/poop area |
print_area |
— | X:0-256, Y:0-256 | Normal printing |
outside |
— | everything else | Fallback |
Used to suppress false AI stall alerts and filament runout events during Canvas filament changes.
Enable with AI_ENABLED=true. Three detection backends run in parallel:
SigLIP zero-shot classification (AI_LOCAL_ENABLED): Runs the Xenova/siglip-base-patch16-224 model locally via @huggingface/transformers. Classifies camera frames against configurable text labels (spaghetti, bed adhesion, stringing, layer shift, warping, blob, empty bed, etc.). SigLIP uses per-label sigmoid scores (each 0–1 independently), normalized to a relative distribution for threshold comparison. Labels are customizable via Settings UI or GET/POST/DELETE /api/config/ai-labels.
Motion-based stall detection: Computes frame-to-frame pixel diff (160×120 grayscale via sharp). If motion drops below 0.5% for 3 consecutive frames while printing, injects a print_stalled issue.
VLM analysis (AI_VLM_ENABLED, off by default): Sends camera snapshots to an external vision-language model (Ollama or OpenAI-compatible API). Can detect issues SigLIP cannot: under_extrusion, nozzle_clog, print_stalled.
Zone-aware filtering: Analysis only runs when sub_status === 2075 (Printing) AND zones.current === 'print_area'. Skipped during heating, filament changes, and when the toolhead is in the cutter/purge area.
Alert logic: Each cycle, critical issues add +2 and warnings add +1 to a consecutive counter (OK decays by -1). When the counter reaches AI_ALERT_THRESHOLD (default 3), an alert fires and is sent to Telegram (if configured) with a camera snapshot. Alerts are rate-limited by AI_ALERT_COOLDOWN (default 300s).
Charts: AI data is shown as 5 score groups: Print in Progress, Spaghetti/Failure, Empty Bed, Paused/Stopped, Other — plus a motion percentage line.
- gcode-preview — Three.js gcode toolpath visualization
- elegoo-link — Elegoo's official C++ SDK
- elegoo-homeassistant — CC2 protocol documentation
- Fluidd — UI design inspiration
- mqtt.js — MQTT client library
MIT