A self-hosted monitoring dashboard for Prusa printers, with an optional OBS
browser overlay. LayerRelay combines a printer camera feed, local PrusaLink
telemetry, recommended Prusa Connect telemetry, decoded .bgcode timelines,
and optional Netatmo room temperature. The same page can run as a normal browser
dashboard, a full 1080p OBS view, or a transparent lower-third overlay.
The project is developed against a Prusa CORE One with an INDX toolchanger. Basic telemetry should be useful with other PrusaLink printers, but their state, camera, and G-code behavior still needs community testing.
The printer integrations are display-only: they read printer APIs with GET
requests and have no pause, stop, movement, temperature, or upload controls.
The local dashboard can write only its own non-secret tool inventory.
That describes this project's behavior, not the authority of a Prusa web-client
refresh token, which may carry broader account permissions; see the
Connect setup guide.
Full 1920 × 1080 camera dashboard · click to open full size
Camera-free telemetry overlay for OBS · click to open full size
- The printer camera, relayed from RTSP through one shared local MJPEG fanout
- Print name and thumbnail, lifecycle state, progress, remaining time, and finish clock
- Nozzle, bed, chamber, and room temperatures
- Active tool, configured spool name/colour, and the next tool change
- Automatic tool count and loaded/material inventory from Prusa Connect, with optional per-field dashboard overrides and locally cached OpenPrintTag suggestions
- Layer, speed, flow, hotend fan, tool-swap count, purge waste, and filament estimate
- Tool-change ticks on the progress bar, decoded from the active
.bgcode - Last-known values during a printer or cloud outage, visibly marked as stale/offline
- A persisted finished-job summary that survives an OBS browser refresh
- Non-print printer activity instead of a false READY/PRINTING state while the machine is calibrating, self-testing, homing, handling filament, or servicing itself
| Component | Required? | Expectation |
|---|---|---|
| Prusa printer | Yes | A network-reachable printer exposing PrusaLink, with a Digest username and password. v0.1.0 is developed and tested with a Prusa CORE One and INDX toolchanger. Other PrusaLink models and firmware combinations are not yet verified; no minimum firmware version is claimed. |
| Prusa Connect account | Recommended for CORE One/INDX | Supplies the exact active tool and richer live telemetry that PrusaLink lacks. The overlay falls back to PrusaLink when Connect is not configured or unavailable. |
| Bun | Native installs | Bun 1.3.14 or newer in the supported 1.x line (>=1.3.14 <2). The Docker image includes Bun. |
| Camera | No | Integrated video requires a reachable rtsp:// or rtsps:// stream. Without one, telemetry and the transparent overlay still work and the camera relay remains off. |
| FFmpeg | Camera relay/snapshot only | Native installs need FFmpeg at cameraFfmpegPath or on PATH for the relay and snapshot helper. The Docker image includes FFmpeg. |
| OBS Studio | No | Needed only for an OBS Browser Source. No minimum OBS version is claimed. Use 1920 x 1080 for the full dashboard or 1920 x 420 for the camera-free lower third. A normal browser can display the dashboard without OBS. |
| Network access | Yes | The host must reach PrusaLink and, when enabled, the RTSP camera. Prusa Connect, optional Netatmo, and OpenPrintTag snapshot refreshes require outbound internet access. LayerRelay loads its last valid snapshot at startup and refreshes a missing or stale snapshot in the background with fixed requests. Picker searches are synchronous and local; picker text is never sent upstream or persisted. Manual tool editing remains usable while suggestions load or during an outage. |
Clone the repository and enter its directory first:
git clone https://github.com/GoByeBye/LayerRelay.git
cd LayerRelayFor complete CORE One/INDX telemetry, first capture a dedicated Prusa Connect refresh token using the Connect setup guide. You can skip that step for a PrusaLink-only setup and add it later.
Use the same setup flow on Windows, macOS, and Linux:
bun ci
bun run setup
bun run doctor
bun run startSetup creates the ignored config.json without overwriting an existing one. In
an interactive terminal it asks for the PrusaLink credentials, optional RTSP
URL, recommended Prusa Connect UUID and hidden refresh token, and an optional
manual tool-count override. Leave the count on auto to follow Connect.
Open http://localhost:8787/ after startup. For a container install, use the Docker guide; Compose publishes the service on host loopback by default.
Move the pointer to reveal Dashboard, then choose Tools & filament.
Tool count, loaded/empty state, and material follow Prusa Connect automatically
when that inventory is available. Count, presence, name, and colour can be
overridden independently and returned to Auto later. Auto type preserves
the selected colour until Auto color is chosen. Changes take
effect without restarting the server. Filament suggestions are optional;
they synchronously search the currently loaded index derived from the
OpenPrintTag material and brand snapshots. Startup loads
DATA_DIR/openprinttag-materials-v1.json, then refreshes a missing or stale
snapshot in the background. Custom names and colours remain usable while that
initial refresh runs or whenever OpenPrintTag is unavailable.
config.example.json is the safe starting point, and config.schema.json
documents the complete configuration shape for editors and validation. At
minimum, set printerHost, username, and password.
The main settings are:
| Setting | Purpose |
|---|---|
listenHost / port |
Local HTTP listener. Keep 127.0.0.1 unless another machine must read the overlay. |
pollIntervalMs |
Local PrusaLink cadence. 2000 is the safe default for the printer's Buddy board. |
sourceCodeUrl |
Corresponding-source URL offered in the dashboard and HTTP Link header. Modified deployments must point it at their exact source. |
toolCount / toolSlots |
Optional inventory overrides. toolCount: null and omitted slot fields follow Prusa Connect; explicit dashboard values persist in DATA_DIR/tool-settings.json. |
toolSettingsAllowedOrigins |
Exact extra browser origins allowed to save tool settings through a named host or authenticated reverse proxy. Loopback and literal IP origins need no entry. |
localBgcodeDirs |
Folders searched for a matching .bgcode before downloading it from the printer. |
printNameOverrides |
Optional exact jobKey to display-name map for slicer files that expose only placeholders such as Merged. |
connect* |
Recommended but experimental Prusa Connect UUID, rotating refresh token, and rate-limited poll cadence. Complete credentials enable it by default; review the service-terms boundary in the setup guide. |
netatmo* |
Optional station credentials and room-temperature poll cadence. |
cameraRtspUrl |
Server-side RTSP source for the shared camera relay and snapshot helper. Buddy3D's local URL contains no credential, but its feed is unprotected; the URL is never sent to the browser. |
cameraStreamEnabled |
Enables the relay. When omitted, a configured cameraRtspUrl enables it automatically; set false to disable it. |
cameraFfmpegPath |
FFmpeg executable name or path. Defaults to ffmpeg on PATH. |
cameraStreamFps / cameraStreamWidth / cameraStreamJpegQuality |
MJPEG frame rate, output width, and encoder quality. Defaults are 24, 1920, and 5. |
cameraStreamThreads |
Decoder/filter/encoder thread cap. Defaults to 4, avoiding FFmpeg's large auto-thread buffer footprint while sustaining 1080p24. |
cameraStreamKillGraceMs |
Grace period before a stuck FFmpeg worker is force-stopped. Defaults to 3000 ms. |
cameraStreamIdleMs / cameraStreamStallMs |
Delay before an unused relay stops and the no-frame watchdog restarts it. Defaults are 10000 and 20000 ms. |
analysisCacheMaxEntries / analysisCacheMaxBytes |
Retention limits for per-print analysis JSON. Defaults are 100 jobs and 64 MiB; camera frames are never stored there. |
Do not commit config.json or cache/: they contain credentials, rotating cloud
tokens, and live state. Both paths are gitignored and excluded from Docker builds.
Custom runtime locations and container environment overrides are documented in
docs/configuration.md.
For a new integrated dashboard, add one Browser source with URL
http://localhost:8787/, Width 1920, and Height 1080. Enable Refresh
browser when scene becomes active. The server opens one upstream RTSP reader
and fans its MJPEG output out to every connected browser, so OBS refreshes or
multiple local previews do not create extra camera sessions.
If OBS already has a Media Source reading the RTSP URL directly, use this order:
- Leave the existing Browser Source at 1920 × 420 while checking that its telemetry still works.
- Disable the old RTSP Media Source. Do this before changing the Browser Source size.
- Set the Browser Source URL to
http://localhost:8787/and resize it to 1920 × 1080. - Confirm that the integrated camera is live, then remove the disabled Media Source when convenient.
That order is important: resizing the Browser Source enables its integrated camera, so disabling the old Media Source first ensures the camera never has two upstream RTSP readers.
An existing 1920 × 420 Browser Source automatically remains the transparent,
camera-free lower third. /overlay and /?camera=0 explicitly select that mode;
/?camera=1 forces the camera on regardless of viewport height. The telemetry
card remains 250 px tall and anchored to the bottom, leaving transparent space
above it in a 420 px source.
Room temperature is hidden initially and is toggled independently in each
Browser Source through the dashboard controls. That preference is stored in the
browser profile. Use ?room=1 or ?room=0 to force it shown or hidden for a
specific source.
Optional query parameters:
?camera=1forces the integrated camera;?camera=0forces transparent overlay mode.?room=1or?room=0overrides the per-browser room-temperature preference.?controls=0hides the auto-fading dashboard controls;?controls=1forces them on.
Prusa firmware reports built-in calibration, self-test, preheat, cold-pull,
filament, and maintenance workflows through one coarse BUSY state. The overlay
therefore always detects that a non-print operation is active and shows a dedicated
activity card instead of READY. It uses a specific label only when an upstream
state, title, or operation field names the task; otherwise it honestly says
Printer busy. It never guesses a calibration type from temperatures or movement.
The repository also builds a static version of the overlay that runs entirely
in the browser and needs no server of its own. A dedicated workflow builds
dist/pages with Bun and deploys it to GitHub Pages on every push to master,
once Settings > Pages > Source is set to GitHub Actions. The published
page has four modes: a generated demo print, local analysis of a dropped
.bgcode or .gcode file, an optional live bridge to a LayerRelay server you
run, and a cloud mode that reads Prusa Connect directly. Decoding, timelines,
thumbnails, and filament search all run in-browser, and a dropped file never
leaves the machine. Browsers cannot poll PrusaLink directly because the printer
firmware sends no CORS headers, so bridge telemetry goes through a LayerRelay
server with apiReadAllowedOrigins configured.
Cloud mode needs no server at all. The browser refreshes an access token against
account.prusa3d.com with a refresh token the printer owner captures from their
own Prusa Connect session, then subscribes to that printer's retained telemetry
topics on Prusa's MQTT broker over a WebSocket. The refresh token stays in that
browser and is sent nowhere but Prusa's account service. That supplies
temperatures, state, progress, remaining time, Z height, the active tool, and
the activity dialog; the thumbnail, layer, swap, and waste timelines come from a
.bgcode dropped onto the page, and there is no camera. A refresh-token chain
must have exactly one consumer, so never share one between cloud mode and a
running LayerRelay server. Setup, the full field list, browser permissions, and
the security boundaries are documented in
docs/static-hosting.md.
PrusaLink does not expose the INDX active tool or swap count as an MMU. Prusa
Connect is therefore the recommended source for the exact active tool and richer
live telemetry; decoded .bgcode remains the source for swap, layer, and waste
timelines. For each new job, the server obtains the .bgcode from the first
applicable source:
- A matching analysis in
cache/ - A matching file in
localBgcodeDirs - An authenticated Prusa Connect download or a printer download, depending on which matching remote descriptor is available for that job
bgcode.js decodes the container in pure JavaScript (Heatshrink + MeatPack), and
toolswaps.js builds progress-to-tool, swap, layer, remaining-time, and purge
timelines. Results are cached by job and decoder version. Local copies are best
for the Buddy board because a printer download can be slow during a busy print.
G-code tools are 0-based (T0-T7); the API keeps currentTool 0-based while
toolLabel and configured toolSlots use the printer UI's 1-based labels.
Purge waste is the extrusion inside PrusaSlicer's FLUSH and EXCLUDE_E blocks,
converted to grams using the file's filament diameter and density. The live
filament readout is explicitly marked as an estimate because print progress is
not an extrusion timeline.
| File | Role |
|---|---|
server.js |
Poll orchestration, state merging, persistence, HTTP API, and static files |
camera-stream.js |
One-upstream RTSP-to-MJPEG relay, client fanout, and FFmpeg lifecycle |
digest.js |
PrusaLink HTTP Digest client |
prusaconnect.js |
Recommended Prusa Connect OAuth and telemetry mapping |
netatmo.js |
Optional Netatmo OAuth and station mapping |
bgcode.js |
.bgcode container and G-code decoder |
toolswaps.js |
Tool/swap/layer/waste timeline builder |
tool-settings.js |
Validated, immediately applied tool inventory persisted under DATA_DIR |
openprinttag-index.js |
Startup-refreshed local suggestion index derived from the public OpenPrintTag material and brand snapshots |
public/overlay.html |
Self-contained overlay UI; browser requests remain same-origin |
pages/ |
Browser ESM ports of the decoder, timelines, a virtual /api engine, and a dependency-free Prusa Connect MQTT client for the static GitHub Pages build |
scripts/build-pages.mjs |
Builds the static dashboard into dist/pages: transformed overlay, bundled browser code, and a generated demo .bgcode |
tools/ |
Guarded restart, camera snapshot, and Connect token-display helpers |
HTTP endpoints:
GET /healthz— process liveness only; printer and camera outages do not fail itGET /source— redirects to the configured corresponding source for the running deploymentGET /api/state— merged live state, connectivity, tool inventory, and completed jobGET /api/settings/tools— non-secret override, detected, and effective tool inventory layers, with anETagrevisionPUT /api/settings/tools— same-origin JSON update for only nullable count and per-field overrides; send the last GET'sETagasIf-Match, and reload after a409conflictGET /api/filaments?q=<text>— synchronous local OpenPrintTag suggestions; custom values never depend on themGET /api/camera.mjpeg— long-lived shared MJPEG printer-camera streamGET /api/camera.jpg— latest complete camera frame as a single JPEGGET /api/camera/status— camera relay state, viewer count, frame age, configured/measured FPS, JPEG byte rate, restart timing, and credential-safe errorsGET /api/jobmap— swap tick percentages for the active analyzed jobGET /api/thumbnail?j=<jobKey>— thumbnail guarded against cross-job reuse
Keep listenHost at 127.0.0.1 for a local OBS setup. Binding to a LAN address
makes the dashboard and read APIs—including the live camera relay—reachable by
other hosts. Restrict the port with the host firewall or an authenticated reverse
proxy. The RTSP URL remains server-side, but the relayed images are still
sensitive.
- Poll loops self-schedule and back off instead of stacking intervals.
- Local telemetry remains authoritative when Prusa Connect is unavailable.
- Last state, completed job, analysis, and rotating OAuth tokens are written atomically and can recover from a backup.
- A disconnected overlay keeps the last-known frame visible and freezes countdowns.
- Analysis and thumbnail failures back off; a late analysis from an old job is not allowed to replace the current job.
- Camera viewers share one FFmpeg/RTSP upstream. The relay starts on demand, drops frames for slow clients instead of buffering without bound, and retries a lost camera connection with backoff.
- Camera JPEGs remain in RAM only: FFmpeg writes to stdout, the relay keeps one latest
frame, browsers receive
no-store, and no recording or frame cache is created. - Per-print analysis JSON is pruned by count and total size.
tools/restart-overlay.ps1preflights the Bun checks, tests, and isolated smoke test before stopping only the server on the configured port. It then waits for the port to clear, starts one hidden process, and verifies/api/state.
pwsh tools/restart-overlay.ps1
bun tools/snapshot.mjs C:\tmp\printer.jpgThe restart helper is Windows-specific; container users should run
docker compose restart overlay. See tools/README.md for snapshot, restart,
and platform notes.
The optional .agents/skills/ files are intentionally public, credential-free
Codex command wrappers for the safe restart and camera-snapshot workflows. They
delegate to the documented tools and are excluded from Docker builds.
bun run check
bun test
bun run smoke
bun run doctor -- --require-runningThe test suite covers camera fanout/backpressure/recovery, dashboard mode and room preferences, material metadata, tool/layer/purge timeline mapping, persistence, request deadlines, telemetry freshness, and Prusa Connect's telemetry and authenticated asset boundaries.
See CONTRIBUTING.md before submitting changes and SECURITY.md for private vulnerability reports.
The repository is licensed under the
GNU Affero General Public License v3 or later. bgcode.js includes a
JavaScript port of Prusa's libbgcode;
NOTICE.md records the pinned provenance, dated modifications, and
the full MeatPack and heatshrink notices. It also contains the project's
AI-assisted development disclosure.
Remote users can open Source & AGPL license from the dashboard controls or
visit GET /source. Modified deployments must set
sourceCodeUrl or SOURCE_CODE_URL to the corresponding source for that exact
version; Docker deployments use SOURCE_CODE_URL, which takes precedence over
the mounted JSON configuration. Package-registry publication remains disabled
with private: true; supported installation paths are a Git checkout or Docker.
