Ship our own Moonraker: the stock 2022 build hides the camera

Mainsail has shown no webcam panel on this printer no matter how the
stream is configured, and the stream was never the problem: nginx serves
/webcam/ correctly and mjpg-streamer answers it.

FlashForge's Moonraker is a 2022 build (API 1.0.5). Moonraker only grew
the webcam "enabled" flag in April 2023, and Mainsail filters its webcam
list on exactly that field -- so every [webcam] entry is discarded and
the panel is removed from the dashboard entirely. Nothing on either side
of that can be configured around; the server has to be newer.

So the mod now ships Moonraker the way it ships Mainsail: pinned in
versions.env, fetched to vendor/ against a sha256, installed over the
stock tree. v0.9.3 is the newest release that runs on the printer's
existing python environment untouched -- v0.10.0 wants dbus-fast, a
compiled module that is not there and would have to be cross-compiled
for mipsel. Only the python package is replaced; the interpreter, the
moonraker-env beside it and moonrakerDaemon stay FlashForge's.

It rides in the mod payload rather than the software component because
the stock run.sh does not extract that component over /usr/prog -- it
copies a hand-written list of paths out of it, and a moonraker/ dropped
in beside them would never be looked at. run-append.sh installs it,
moving the old tree aside and putting it back if the copy fails: this is
the one thing the mod writes to /usr/prog that the stock installer does
not, and a printer whose Moonraker did not survive has no web UI at all.

The database migrates itself -- v0.9.x moved from lmdb to sqlite, lmdb
1.3.0 is installed, so Mainsail's settings survive the upgrade.

Covered end to end on the printer replica: that the swap happened, that
the installed webcam.py has the enabled field, and that the shipped tree
really runs on the printer's own python3.8 under qemu-mipsel. With
BUILD_MOONRAKER=0 those skip and the stock server is left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oleksandr ShyshatskyiandClaude Opus 5 committed 2026-08-24 17:42:34 +03:00
1 parent 3e997e5a44
commit a40bc861dd
11 files changed
+226 -18

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ does the work.
| **Mainsail in your browser** | The full Klipper web interface at `http://<printer-ip>/` — upload gcode, watch the print, tune on the fly. Moonraker comes with it, so anything that speaks the Klipper API works. |
| **A shell on your printer** | ssh as root. Dropbear is already running on stock firmware; this simply gives you a password you know — the installer picks a random one and writes it to `anvil-password.txt` on your USB stick. |
| **Real toolchanger Klipper** | A current Klipper with proper tool-change support, replacing the 0.12-era tree FlashForge ships. |
| **A current Moonraker** | Replacing the 2022 build FlashForge ships, which is too old for today's Mainsail — with it, the camera panel never appears at all. |
| **A modern touchscreen UI** | HelixScreen replaces the stock interface on the printer's own screen. |
| **Nothing you cannot undo** | Flashing the stock FlashForge package puts every file back. |
+9 -6
View File
@@ -34,20 +34,23 @@ fi
BUILD_KLIPPER="${BUILD_KLIPPER:-fork}"
BUILD_TOOLCHANGE="${BUILD_TOOLCHANGE:-1}"
BUILD_MAINSAIL="${BUILD_MAINSAIL:-1}"
BUILD_MOONRAKER="${BUILD_MOONRAKER:-1}"
BUILD_HELIX="${BUILD_HELIX:-1}"
MOD_SSH="${MOD_SSH:-1}"
MOD_WEB="${MOD_WEB:-1}"
# Third-party payload pieces (Mainsail, HelixScreen). They are downloaded on
# demand rather than vendored, so the repo carries no binaries and no binary
# history. versions.env pins the version and the sha256; bin/fetch-assets.sh
# puts the file in vendor/. Point MAINSAIL_ZIP / HELIX_TGZ at your own build
# in config.env to override -- an explicit path is never overwritten.
# Third-party payload pieces (Mainsail, HelixScreen, Moonraker). They are
# downloaded on demand rather than vendored, so the repo carries no binaries
# and no binary history. versions.env pins the version and the sha256;
# bin/fetch-assets.sh puts the file in vendor/. Point MAINSAIL_ZIP /
# HELIX_TGZ / MOONRAKER_TGZ at your own build in config.env to override -- an
# explicit path is never overwritten.
# shellcheck disable=SC1091
[ -f "$ROOT/versions.env" ] && . "$ROOT/versions.env"
MAINSAIL_ZIP="${MAINSAIL_ZIP:-$ROOT/vendor/mainsail-${MAINSAIL_VERSION:-unpinned}.zip}"
HELIX_TGZ="${HELIX_TGZ:-$ROOT/vendor/${HELIX_FILE:-helixscreen.tar.gz}}"
export MAINSAIL_ZIP HELIX_TGZ
MOONRAKER_TGZ="${MOONRAKER_TGZ:-$ROOT/vendor/moonraker-${MOONRAKER_VERSION:-unpinned}.tar.gz}"
export MAINSAIL_ZIP HELIX_TGZ MOONRAKER_TGZ
# Replica-only settings: the factory image and the partition sizes. They exist
# for the tests and never reach a printer, so they live in their own file --
+7
View File
@@ -55,3 +55,10 @@ if [ "$ALL" = 1 ] || [ "${BUILD_HELIX:-0}" = "1" ]; then
get "https://github.com/Klipper4Creator5/helixscreen/releases/download/$HELIX_VERSION/$HELIX_FILE" \
"$HELIX_TGZ" "$HELIX_SHA256"
fi
if [ "$ALL" = 1 ] || [ "${BUILD_MOONRAKER:-0}" = "1" ]; then
# Moonraker ships no release asset, so this is GitHub's generated source
# tarball for the tag. The sha256 in versions.env is what makes that safe.
get "https://github.com/Arksine/moonraker/archive/refs/tags/$MOONRAKER_VERSION.tar.gz" \
"$MOONRAKER_TGZ" "$MOONRAKER_SHA256"
fi
+1 -1
View File
@@ -68,7 +68,7 @@ ls -lh "work/stage/software-$OUT_VER.tar.xz" | awk '{print " "$5}'
if [ -d work/modpayload ]; then
# This one IS really xz: we extract it ourselves with `xz -dc`, and
# FlashForge's factory installer proves xz exists on the printer.
echo ">> compressing anvil.tar.xz (Mainsail / HelixScreen / bin)"
echo ">> compressing anvil.tar.xz (Mainsail / HelixScreen / Moonraker / bin)"
tar -cf - -C work/modpayload . | xz -T0 -6 > work/stage/anvil.tar.xz
ls -lh work/stage/anvil.tar.xz | awk '{print " "$5}'
fi
+71 -7
View File
@@ -127,7 +127,71 @@ else
fi
[ -f assets/moonraker.conf ] && cp -f assets/moonraker.conf "$MP/config/moonraker.conf"
# ----------------------------------------------------------- 4. HelixScreen
# ------------------------------------------------------------- 4. Moonraker
# WHY THIS EXISTS -- the stock Moonraker is a 2022 build (it reports API
# 1.0.5) and it does NOT come from the update package at all: it ships on the
# factory image only, at /usr/prog/moonraker/moonraker/. Old enough that the
# current Mainsail quietly drops features it cannot see. The camera is the one
# you notice: Moonraker only grew the webcam "enabled" flag in April 2023, and
# Mainsail filters its webcam list on exactly that field, so every [webcam]
# entry is discarded and the panel disappears -- with the stream itself
# perfectly healthy behind nginx. No amount of config fixes that; the server
# has to be newer.
#
# WHAT IS AND IS NOT REPLACED. Only the python package tree
# (moonraker/moonraker/moonraker/) is swapped. The interpreter, the
# moonraker-env virtualenv beside it and moonrakerDaemon are all left alone,
# because v0.9.3 runs on what the printer already has:
#
# tornado 6.1, jinja2 3.1.2, distro 1.5.0, libnacl 1.7.2,
# streaming-form-data 1.8.1, inotify-simple 1.3.5, importlib_metadata 5.1.0,
# dbus-next 0.2.3
#
# -- verified by booting v0.9.3 against exactly those versions on python 3.8,
# started the same way moonrakerDaemon starts it (moonraker/moonraker.py -d).
# Nothing needs a MIPS wheel built: the only native module Moonraker imports
# is streaming_form_data._parser, and the installed 1.8.1 already exports
# every name v0.9.3 asks of it (StreamingFormDataParser, ParseFailedException,
# FileTarget, ValueTarget, SHA256Target).
#
# WHY v0.9.3 AND NOT v0.10.0. v0.10.0 swapped dbus-next for dbus-fast, a
# compiled Cython module that is not on the printer and would have to be
# cross-compiled for mipsel. v0.9.3 is the newest release that needs nothing
# built, and it is already well past the webcam fix.
#
# The database converts itself. v0.9.x moved the store from lmdb to sqlite and
# migrates the old one on first boot; lmdb 1.3.0 is installed, so it can still
# read what is there and Mainsail's settings survive the upgrade.
#
# WHY IT RIDES IN THE MOD PAYLOAD AND NOT THE SOFTWARE COMPONENT. The stock
# run.sh does not extract the software component over /usr/prog -- it copies a
# hand-written list of paths out of it (app_startup.sh, klipper/klippy/*,
# firmwareExe, ...). A moonraker/ directory dropped in beside them would be
# unpacked to /usr/prog/PROGRAM/software/<ver>/ and then simply sat there.
# So it travels with the rest of the payload and run-append.sh puts it in
# place, which also lets that step swap the tree atomically and roll back.
if [ "${BUILD_MOONRAKER:-1}" = "1" ]; then
[ -f "${MOONRAKER_TGZ:-}" ] || { echo "BUILD_MOONRAKER=1 but no Moonraker tarball at '${MOONRAKER_TGZ:-}' -- run ./bin/fetch-assets.sh" >&2; exit 1; }
say "Moonraker: staging $MOONRAKER_VERSION package tree"
rm -rf work/.moonraker
mkdir -p work/.moonraker
tar -xzf "$MOONRAKER_TGZ" -C work/.moonraker --strip-components=1
# Guard against a tarball whose shape changed under us -- silently
# shipping nothing here would look like a clean build and a dead UI.
[ -f work/.moonraker/moonraker/moonraker.py ] || {
echo " !! no moonraker/moonraker.py in $(basename "$MOONRAKER_TGZ")" >&2; exit 1; }
rm -rf "$MP/moonraker"
cp -a work/.moonraker/moonraker "$MP/moonraker"
# Tests never run on the printer and are a sizeable chunk of the tree.
rm -rf "$MP/moonraker/tests"
find "$MP/moonraker" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null
rm -rf work/.moonraker
du -sh "$MP/moonraker" | awk '{print " "$1}'
else
skip "Moonraker: keeping the stock 2022 build"
fi
# ----------------------------------------------------------- 5. HelixScreen
if [ "${BUILD_HELIX:-1}" = "1" ]; then
[ -f "${HELIX_TGZ:-}" ] || { echo "BUILD_HELIX=1 but no HelixScreen tarball at '${HELIX_TGZ:-}' -- run ./bin/fetch-assets.sh" >&2; exit 1; }
say "HelixScreen: unpacking $(basename "$HELIX_TGZ")"
@@ -144,7 +208,7 @@ else
skip "HelixScreen"
fi
# ------------------------------------------------------------------- 5. SSH
# ------------------------------------------------------------------- 6. SSH
# Nothing to install. The stock rootfs (kernel-*.tar.xz -> rootfs.squashfs)
# already ships /usr/sbin/dropbear, /usr/bin/dropbearkey AND an enabled
# /etc/init.d/S50dropbear that busybox init runs at every boot. SSH is
@@ -168,7 +232,7 @@ if [ -n "${BUSYBOX_BIN:-}" ] && [ -f "$BUSYBOX_BIN" ]; then
cp -f "$BUSYBOX_BIN" "$MP/bin/busybox"; chmod +x "$MP/bin/busybox"
fi
# --------------------------------------------------------- 6. root password
# --------------------------------------------------------- 7. root password
if [ -n "${ROOT_PW_HASH:-}" ]; then
say "Accounts: setting root password hash"
# /etc is a bind mount of /usr/prog/etc (app_startup.sh), and this file is
@@ -181,12 +245,12 @@ else
skip "root password (set ROOT_PW_HASH)"
fi
# ------------------------------------------------ 7. start.sh (web stack on)
# ------------------------------------------------ 8. start.sh (web stack on)
say "start.sh: enabling nginx + moonraker"
cp -f payload/start.sh "$SW/start.sh"
chmod +x "$SW/start.sh"
# ------------------------------------- 8. firmwareExe -> our wrapper script
# ------------------------------------- 9. firmwareExe -> our wrapper script
# The stock chain is rcS -> S99factory_test_shell -> app_startup.sh ->
# firmwareExe, and firmwareExe is also what starts Klipper. Replacing this
# one file is therefore enough to own the whole userspace boot, which means
@@ -200,7 +264,7 @@ say "firmwareExe: installing wrapper (replaces the stock binary)"
cp -f payload/firmwareExe "$SW/firmwareExe"
chmod +x "$SW/firmwareExe"
# ------------------------------------------------------ 9. mod service dir
# ----------------------------------------------------- 10. mod service dir
mkdir -p "$MP/init.d"
[ -d payload/bin ] && cp -f payload/bin/* "$MP/bin/" && chmod +x "$MP/bin"/*
cp -f payload/init.d/S* "$MP/init.d/"
@@ -211,7 +275,7 @@ sed -e "s/^MOD_WEB=.*/MOD_WEB=${MOD_WEB:-1}/" \
-e "s/^MOD_WIFI=.*/MOD_WIFI=${MOD_WIFI:-1}/" \
payload/anvil.conf > "$MP/anvil.conf"
# --------------------------------------------------- 10. run.sh install step
# --------------------------------------------------- 11. run.sh install step
say "run.sh: injecting mod install blocks (pre + post)"
POST=work/.run-post.sh
# 1 only when ssh is on and nothing was baked in: a package is one file that
+5
View File
@@ -112,6 +112,11 @@ if [ -f "$T/anvil.tar.xz" ]; then
echo "$LIST" | grep -q 'mainsail/index.html' && ok "Mainsail present" || warn "no Mainsail in payload"
echo "$LIST" | grep -q 'helixscreen/bin/helix-screen' && ok "HelixScreen present" || warn "no HelixScreen in payload"
echo "$LIST" | grep -q 'bin/dropbear' && ok "dropbear present" || warn "no dropbear -- ssh will not start"
# moonraker.py is the file moonrakerDaemon execs by absolute path; a
# payload with the directory but not that file installs a Moonraker that
# cannot start, which looks identical to a dead printer from the outside.
echo "$LIST" | grep -q 'moonraker/moonraker.py' && ok "Moonraker present" \
|| warn "no Moonraker in payload -- the stock 2022 build stays, and Mainsail will hide the webcam"
else
warn "no anvil.tar.xz -- scripts only"
fi
+3 -3
View File
@@ -69,9 +69,9 @@ BUSYBOX_BIN="$DESKTOP/creator5/busybox" # optional richer busybox
# printer re-reads at every boot, so they can be changed over ssh afterwards.
#
# The BUILD_* flags that decide what goes INTO a package (BUILD_KLIPPER,
# BUILD_MAINSAIL, BUILD_HELIX, BUILD_TOOLCHANGE) are not set here: bin/common.sh
# defaults them to the full firmware, and setting one in this file overrides
# that. Only touch them if you want a package with a piece left out.
# BUILD_MAINSAIL, BUILD_MOONRAKER, BUILD_HELIX, BUILD_TOOLCHANGE) are not set
# here: bin/common.sh defaults them to the full firmware, and setting one in
# this file overrides that. Only touch them if you want a piece left out.
MOD_WEB=1 # 1 = start nginx (Mainsail) + moonraker
MOD_CAM=1 # 1 = start mjpg-streamer on :8080 (firmwareExe used to)
MOD_SSH=1 # 1 = start dropbear on port 22
+28
View File
@@ -52,6 +52,34 @@ goes to `/usr/prog` on the firmware partition, which only holds two versions
and cannot fit ~100MB of web UI — that is why Mainsail and HelixScreen ride in
the outer package instead.
## Moonraker
The mod ships its own Moonraker (pinned in `versions.env`) and installs it over
the stock one. FlashForge's is a 2022 build that reports API 1.0.5, old enough
that the current Mainsail hides features it cannot see. The visible one is the
camera: Moonraker only grew the webcam `enabled` flag in April 2023, Mainsail
filters its webcam list on exactly that field, and so every `[webcam]` entry is
discarded and the panel disappears — while the stream itself is perfectly
healthy behind nginx. No config change can fix that from either side.
Only the python package tree is replaced. The interpreter, the `moonraker-env`
beside it and `moonrakerDaemon` are FlashForge's and keep working, because the
pinned version runs on the libraries already installed — nothing has to be
cross-compiled for mipsel. That is the reason for the version choice: v0.9.3 is
the newest release that needs nothing built (v0.10.0 wants `dbus-fast`, a
compiled module the printer does not have).
Moonraker does not come from the update package at all — it exists only on the
factory image, and the stock `run.sh` copies a hand-written list of paths out
of the software component rather than extracting it over `/usr/prog`. So the
tree travels with the mod payload and `run-append.sh` puts it in place, moving
the old one aside first and restoring it if the copy does not complete.
`make test` covers this end to end on the printer replica: that the swap
happened, that the installed `webcam.py` has the `enabled` field, and that the
shipped tree really runs on the printer's own python3.8. `BUILD_MOONRAKER=0`
builds a package that leaves the stock server alone.
## Five things the stock firmware does that will surprise you
Each of these silently breaks a package or a printer, and each is enforced by
+47 -1
View File
@@ -28,7 +28,7 @@ if [ -n "$MODTAR" ]; then
else
# Keep user-editable state; replace everything we own.
[ -f $MODDIR/anvil.conf ] && cp -f $MODDIR/anvil.conf /tmp/anvil.conf.keep
rm -rf $MODDIR/bin $MODDIR/www $MODDIR/nginx $MODDIR/helixscreen $MODDIR/config
rm -rf $MODDIR/bin $MODDIR/www $MODDIR/nginx $MODDIR/helixscreen $MODDIR/config $MODDIR/moonraker
mkdir -p $MODDIR
# Try xz first (FlashForge's own factory installer uses `xz -dc`, so
# it exists), then fall back to plain tar in case a build shipped it
@@ -49,6 +49,52 @@ else
fi
sync
# ---- Moonraker -------------------------------------------------------------
# Replace the stock 2022 Moonraker with the one bin/patch.sh staged. Only the
# python package is swapped -- the interpreter, the moonraker-env beside it and
# moonrakerDaemon are FlashForge's and keep working, because the version we
# ship runs on the libraries already installed. See bin/patch.sh for why that
# is true and why this is the only way the camera can appear in Mainsail.
#
# This is the one thing the mod writes to /usr/prog that is not part of the
# software component, so it is done defensively: the old tree is moved aside
# rather than deleted, and put back if the copy does not complete. A printer
# whose Moonraker did not survive an update has no web UI at all, and the
# screen would be the only way to notice.
if [ -d $MODDIR/moonraker ]; then
MRROOT=/usr/prog/moonraker/moonraker
if [ -d $MRROOT/moonraker ]; then
# Both trees exist at once during the swap; /usr/prog is the small
# firmware partition, so check there is room before starting.
NEED=`du -sk $MODDIR/moonraker | cut -f1`
FREE=`df /usr/prog | tail -1 | tr -s ' ' | cut -d' ' -f4`
if [ "${FREE:-0}" -lt "$NEED" ]; then
echo "!! moonraker: only ${FREE}KB free on /usr/prog, need ${NEED}KB -- keeping the stock tree"
else
rm -rf $MRROOT/moonraker.modold
if mv $MRROOT/moonraker $MRROOT/moonraker.modold; then
if cp -a $MODDIR/moonraker $MRROOT/moonraker; then
sync
# Bytecode from the FlashForge build would otherwise be
# the first thing imported. Same trap as klippy below.
find $MRROOT/moonraker -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null
rm -rf $MRROOT/moonraker.modold
echo "moonraker: replaced with the mod's build"
else
rm -rf $MRROOT/moonraker
mv $MRROOT/moonraker.modold $MRROOT/moonraker
echo "!! moonraker: copy failed -- rolled back to the stock tree"
fi
else
echo "!! moonraker: could not move the stock tree aside -- left it alone"
fi
fi
else
echo "!! moonraker: no $MRROOT/moonraker on this printer -- nothing replaced"
fi
sync
fi
# ---- klipper + moonraker configs -------------------------------------------
# Every file here is one the mod ships (ff-*.cfg, moonraker.conf); printer.cfg
# is the user's and is never shipped, so it is never a candidate.
+46
View File
@@ -293,6 +293,52 @@ else
fi
killall sleep 2>/dev/null
# ---- Moonraker was actually replaced ---------------------------------------
# The stock Moonraker is a 2022 build that predates the webcam "enabled" flag,
# and Mainsail drops every webcam that lacks it -- so a silently skipped swap
# looks like a healthy printer with no camera. run-append.sh reports into the
# install log, which is why the log line is checked and not just the files.
MRPKG=/usr/prog/moonraker/moonraker/moonraker
if [ -d $MRPKG ]; then
# Only assert the swap for a package that actually carries Moonraker --
# BUILD_MOONRAKER=0 is a supported way to build one that does not, and it
# should leave the stock server alone rather than fail the run.
if [ -d /usr/data/anvil/moonraker ]; then
grep -q "moonraker: replaced with the mod's build" /usr/data/anvil-install.log 2>/dev/null \
&& ok "moonraker: the install replaced the stock tree" \
|| bad "moonraker: run-append.sh did not report a replacement"
# The field the whole exercise is about. Its absence means an old tree.
grep -q '"enabled"' $MRPKG/components/webcam.py 2>/dev/null \
&& ok "moonraker: the installed webcam component has the enabled field" \
|| bad "moonraker: webcam.py has no enabled field -- Mainsail will hide the camera"
else
echo " (skip) moonraker: this package ships none (BUILD_MOONRAKER=0)"
fi
# moonrakerDaemon execs this by absolute path; nothing else starts it.
[ -f $MRPKG/moonraker.py ] \
&& ok "moonraker: moonraker.py is where moonrakerDaemon looks for it" \
|| bad "BRICK: no $MRPKG/moonraker.py -- Moonraker cannot start"
# The rollback copy must never be left behind: it is a second full tree on
# the small firmware partition.
[ -d $MRPKG.modold ] \
&& bad "moonraker: the swap left its rollback copy on /usr/prog" \
|| ok "moonraker: no rollback copy left behind"
# THE ONE THAT MATTERS. Everything above only says the right files are on
# disk; this says the printer's own python3.8 can actually run them. We
# ship a Moonraker newer than the machine's, on libraries FlashForge
# installed and we do not control, so an ImportError here is the failure
# mode to be afraid of -- and it is real mipsel under qemu, not a mock.
# S60web starts it during boot 3, so it has had time by now.
if wait_for 60 running 'moonraker/moonraker.py'; then
ok "moonraker: the shipped tree runs on the printer's python3.8"
else
bad "moonraker: nothing is running moonraker.py -- the new tree did not start"
tail -25 /usr/data/logs/moonraker.log 2>/dev/null | sed 's/^/ /'
fi
else
bad "moonraker: no $MRPKG -- this prog partition has no Moonraker to replace"
fi
# Nothing may be written to the read-only root. A mod that needs it works in a
# permissive sandbox and fails silently on the machine.
if [ -f /tmp/sim-neutered.log ]; then
+8
View File
@@ -12,3 +12,11 @@ MAINSAIL_SHA256="df2ba7c301f7bfc8ac9f122741a6ba08356d679ecfa1f62f898d0337802d5de
HELIX_VERSION="v0.99.115-creator5.1"
HELIX_FILE="helixscreen-creator5-v0.99.115.tar.gz"
HELIX_SHA256="7205738ca8ccb0aa9a95262506a87283712ab5544ce4abdbcbee571f51395a61"
# Moonraker. The stock one is a 2022 build (API 1.0.5) that predates the
# webcam "enabled" flag that modern Mainsail requires -- see bin/patch.sh.
# v0.9.3 is the newest release that runs on the printer's stock python
# environment untouched; v0.10.0 needs dbus-fast, a compiled module that is
# not there. A source tarball, not a release asset: Moonraker publishes none.
MOONRAKER_VERSION="v0.9.3"
MOONRAKER_SHA256="25b3bccb62d5b82505ea90faf8f4e3b3fb274e8531b52530ac494fb624421864"