User Guide
Configure & deploy
Run the web admin, understand the config file, then package a native installer and turn a Raspberry Pi into a kiosk.
Web admin
Backend is Ktor (Kotlin/JVM, org.speculum.configserver); the frontend is React + Vite + TypeScript. The app and the server share one config.json — edit it here and the mirror reloads automatically.
Development (two processes)
# 1) backend on :8080 (run from repo root so it shares config.json + plugins/)
MIRROR_ADMIN_PASSWORD=secret ./gradlew :config-server:run
# 2) UI on :5173 (proxies /api → :8080)
cd config-server/web && npm install && npm run dev
# open http://localhost:5173
Production (single origin)
cd config-server/web && npm install && npm run build # → web/dist
./gradlew :config-server:run # serves UI + API on :8080
# open http://localhost:8080
How config resolves: a non-empty modules list is authoritative; an empty list means "use every installed plugin's default placement" (the out-of-box mirror). The admin's first load shows all modules; saving captures an explicit, editable set.
Environment variables
| Var | Default | Purpose |
|---|---|---|
MIRROR_ADMIN_PASSWORD | admin | Bootstrap password (see below) |
MIRROR_ADMIN_SECRET | built-in string | HMAC key for tokens — set in prod |
MIRROR_ADMIN_PORT | 8080 | HTTP port |
MIRROR_ADMIN_DISABLED | — | 1 disables the config server entirely |
MIRROR_CONFIG | config.json (cwd) | Config file shared with the app |
MIRROR_PLUGINS | plugins/ (or app resources) | Where to scan modules for the picker |
Password & security
Change the admin password from the UI (Security card) or via POST /api/password. Once changed it is persisted as a salted PBKDF2-HMAC-SHA256 hash in admin-auth.json, next to the config file (~/.speculum/ by default) — the plaintext is never stored.
MIRROR_ADMIN_PASSWORD applies until the first change writes admin-auth.json, after which the stored hash wins and the env var is ignored. Delete admin-auth.json to revert. New passwords need ≥ 4 characters. In production, always set MIRROR_ADMIN_SECRET.Package a native build
The app builds a native .deb via Compose Desktop + jpackage, bundling a JRE, all plugin JARs, and the web admin. One process runs both the mirror and the admin on :8080.
jpackage only produces installers for the OS + CPU arch it runs on. Build a Raspberry Pi .deb on the Pi itself or any arm64 Linux box. Use 64-bit Raspberry Pi OS (arm64) — Skiko ships linux-arm64 natives, not 32-bit armhf.Prerequisites on the Pi, then build:
sudo apt update
sudo apt install -y openjdk-17-jdk fakeroot binutils dpkg
git clone https://github.com/pierrejochem/SpeculumSmartMirror.git && cd SpeculumSmartMirror
(cd config-server/web && npm install && npm run build) # build the admin UI first
./gradlew packageDeb
# → composeApp/build/compose/binaries/main/deb/speculum_1.0.0-1_arm64.deb
Also available: ./gradlew packageRpm (needs rpmbuild), and a PKGBUILD for Arch (makepkg -si on an aarch64 box).
Install & run
sudo apt install ./composeApp/build/compose/binaries/main/deb/speculum_1.0.0-1_arm64.deb
/opt/speculum/bin/speculum # fullscreen mirror + admin on :8080
- One launch starts both the mirror and the admin. Open
http://<pi-ip>:8080from any device on the network (default passwordadmin). Saving there hot-reloads the running mirror. - Config lives at
~/.speculum/config.json(override with-Dmirror.config=…orMIRROR_CONFIG). - Plugin JARs + web UI load from
/opt/speculum/lib/app/resources/{plugins,web}, independent of cwd.
Run on boot (kiosk)
Speculum runs as a desktop-session autostart: the mirror launches fullscreen when your desktop logs in. This is the most reliable kiosk on Raspberry Pi OS — the session already owns the display, seat, and runtime dir, so there's none of the seat / XDG_RUNTIME_DIR fragility of a bare compositor. The package ships a one-shot helper:
# run as your normal desktop user (NOT root)
speculum-kiosk-enable # installs a per-user autostart entry
# log out and back in (or reboot) — the mirror comes up fullscreen
speculum-kiosk-disable # undo
speculum-kiosk-enable writes ~/.config/autostart/speculum.desktop pointing at a wrapper (/opt/speculum/libexec/speculum-kiosk-run) that also disables screen blanking and hides the idle cursor before starting the mirror (which opens fullscreen on its own).
raspi-config → System Options → Boot / Auto Login → Desktop Autologin — and the mirror appears unattended after a reboot.The autostart entry is honoured by the Pi OS desktops — LXDE and the Wayland labwc / wayfire sessions — via the XDG autostart spec. Speculum is an X11/AWT app and runs through Xwayland automatically on the Wayland sessions.
Cursor & blanking on Wayland
The wrapper's xset / unclutter hardening is X11-only (it works under Xwayland for the cursor). On a pure Wayland compositor, disable blanking in the compositor instead — wayfire's [idle] settings, or swayidle — or just turn the Pi's screen blanking off in raspi-config → Display Options. Install unclutter for cursor hiding: sudo apt install -y unclutter.
8080. See Troubleshooting → two mirrors.Low-power tuning (Pi 3B and similar)
Measured idle: ~45 MB live heap, ~390 MB RSS with default sizing. On a 1 GB Pi 3B that's tight, and the VideoCore IV GPU has weak desktop-OpenGL support, so Compose/Skiko likely renders in software — fine for static content, not smooth animation.
- Turn off seconds — set the clock's
displaySeconds=false(web admin) so it recomposes once a minute. Biggest CPU win. - Use a lightweight desktop (LXDE, or the Wayland labwc session) rather than a heavy one — the mirror autostarts into it.
- Enable the KMS GL driver and give the GPU memory (
raspi-config) — may let Skiko use hardware GL. - Prefer a Pi 4 (2–4 GB); the 3B works but is marginal.
Already applied in nativeDistributions.jvmArgs: -Xmx160m -XX:+UseSerialGC -XX:MaxMetaspaceSize=96m.
Verifying releases
Every release ships a SHA256SUMS covering all packages (.deb / .rpm / .pkg.tar.*):
sha256sum -c SHA256SUMS --ignore-missing
When signing is configured, each asset and SHA256SUMS also gets a detached .asc GPG signature. The public key is published in KEYS:
curl -fsSL https://raw.githubusercontent.com/pierrejochem/SpeculumSmartMirror/main/KEYS | gpg --import
gpg --verify SHA256SUMS.asc SHA256SUMS
gpg --verify speculum_<version>_arm64.deb.asc speculum_<version>_arm64.deb
Key fingerprint: 4F73521A2DB000391BEF252AAD96D4494AF02FA1