Start the camera ourselves: mjpg-streamer was on the disk all along

Correcting the previous commit, which said the :8080 stream could not be
restored without cross-compiling a streamer. Both halves are actually true:

  * firmwareExe really did serve the stream itself -- its own accept loop,
    `GET /?action=stream`, boundarydonotcross, even announcing
    `Server: MJPG-Streamer/0.2` -- so killing it does take the camera down.
  * ...but /usr/prog/mjpg-streamer/ ships the genuine article: mjpg_streamer,
    input_uvc.so, output_http.so, its own libjpeg.so.9 and www/.
    app_startup.sh:159 exports LD_LIBRARY_PATH for it and then never launches
    it. Nothing needs building; the binary just needs starting.

S65camera does that, on the S50wifi pattern -- the wait for /dev/video0 and the
respawn loop run in a background subshell, so a printer with no camera plugged
in never holds up Klipper or the UI. stop() kills the supervisor before the
streamer, or it would come back three seconds later.

Two details are load-bearing and easy to lose: libjpeg.so.9 lives in the
mjpg-streamer directory, so without LD_LIBRARY_PATH the binary dies on a
missing shared object (the same trap moonrakerDaemon has in S60web); and the
plugin .so names resolve against the working directory, hence the cd.

nginx proxies it at /webcam/ with buffering off and no read timeout, since
MJPEG is one endless response, and moonraker.conf's [webcam] entry uses
relative URLs through that proxy -- so one forwarded port carries the stream,
tunnels included. MOD_CAM=0 turns it off; MOD_CAM_RES/FPS/PORT tune it.

The earlier claim is corrected in docs/notes/25 and in the README, which no
longer lists the camera as something you give up.

Verified: parses under the printer's own busybox ash, lint-danger clean. The
full suite was not re-run to completion -- the working tree is being edited
concurrently and a build lane raced it (tar: file changed as we read it).
bin/patch.sh's MOD_CAM line is deliberately left out of this commit for the
same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oleksandr ShyshatskyiandClaude Opus 5 committed 2026-08-23 22:06:25 +03:00
1 parent 5bdf78109a
commit 607e6595fb
8 files changed
+195 -53

No files matched your search

+19 -17
View File
@@ -30,18 +30,19 @@ as it was.
### What you give up
That one file is FlashForge's whole application, and two things people use
live inside it rather than in some separate service. Both stop working:
That one file is FlashForge's whole application, so anything that lived inside
it rather than in a separate service goes with it:
* **The camera.** The stream on `:8080` is served by the app itself, not by a
camera daemon, so it goes when the app does. Mainsail shows no webcam.
Restoring it needs a streamer built for this board — not done yet.
* **The FlashForge network API on `:8898`**, and with it FlashPrint, Orca's
FlashForge profile and the mobile app. Moonraker's API takes over, which is
what Mainsail, OrcaSlicer's Klipper/Moonraker upload and Fluidd speak.
Slice for the toolchanger and send it there instead.
Flashing the stock package brings both back.
The camera is **not** on that list, though the app did serve it: mjpg-streamer
is already on the printer, unused, and this firmware simply starts it. The
stream shows up in Mainsail as usual.
Flashing the stock package brings the API back.
---
@@ -73,18 +74,20 @@ Not sure which you have? On the printer: **Settings → About**.
Afterwards, `http://<printer-ip>/` gets you Mainsail. The install log is on
the printer at `/usr/data/anvil-install.log`.
### Flash the probe first
### Before you do
There are two files per model, and they are not a menu — flash them in order:
One file per model, one flash — but do two things first, because they are the
cheap version of every problem people hit:
| Stage | What it changes | Why |
|---|---|---|
| **probe** | **nothing at all** | Proves the whole update chain works on *your* machine, and writes a report back onto the USB stick. Costs you five minutes and rules out a bad stick, the wrong model or a truncated download. |
| **firmware** | everything: Mainsail, ssh, toolchanger Klipper, HelixScreen | The actual firmware. Flash it once the probe has come back clean. |
* **Check the model.** A `Creator5Pro-` package will not install on a Creator 5
and vice versa; the printer refuses it outright, which is annoying rather
than dangerous, but it is the most common reason a flash "does nothing".
* **Put the stock FlashForge package for your model on a second stick**, and
keep it separate. That stick is the undo button — see below.
The go/no-go checks — what to look at after each flash, and what means stop —
are in **[docs/hardware-testing.md](docs/hardware-testing.md)**. Read it before
the first flash.
The go/no-go checks — what to look at after the flash, in what order, and what
means stop — are in **[docs/hardware-testing.md](docs/hardware-testing.md)**.
Read it before the first flash.
---
@@ -123,8 +126,7 @@ machine's own boot script finds it, installs it, and boots again with the mod
running. That replica is the printer's real filesystem running its real
binaries. It is still not a machine.
Flash the probe first, and have the stock package on a spare stick before you
begin.
Have the stock package for your model on a spare stick before you begin.
---
+12 -15
View File
@@ -32,19 +32,16 @@ trusted_clients:
[history]
# No [webcam] entry, and none is possible yet.
# The camera. mjpg-streamer ships on the firmware partition
# (/usr/prog/mjpg-streamer) and S65camera starts it on :8080 -- firmwareExe
# used to be what launched it, and served the stream itself besides.
#
# The stock MJPEG stream on :8080 was not mjpg-streamer: firmwareExe served it
# itself, from an HTTP server built into the binary (`GET /?action=stream`,
# multipart/x-mixed-replace, its own accept loop). The mod replaces firmwareExe,
# so the stream goes with it and there is no daemon left to point a [webcam] at.
#
# Restoring it needs a streamer cross-compiled for MIPS (ustreamer or
# mjpg-streamer) shipped in the payload and started from an init script, the
# way S50wifi covers the other job firmwareExe used to do. Once that exists:
#
#[webcam anvil]
#location: printer
#service: mjpegstreamer-adaptive
#stream_url: /webcam/?action=stream
#snapshot_url: /webcam/?action=snapshot
# The URLs are relative on purpose: they go through nginx's /webcam/ proxy, so
# the stream works over whatever single port you reach the printer on, tunnels
# included. Set MOD_CAM=0 in anvil.conf if there is no camera.
[webcam anvil]
location: printer
service: mjpegstreamer-adaptive
target_fps: 15
stream_url: /webcam/?action=stream
snapshot_url: /webcam/?action=snapshot
+12
View File
@@ -64,6 +64,18 @@ http {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# The camera, so one forwarded port is enough to see the stream.
# S65camera starts mjpg-streamer here; with MOD_CAM=0 this 502s, which
# is the honest answer for "no camera configured".
location /webcam/ {
proxy_pass http://127.0.0.1:8080/;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
# MJPEG is one endless response: no buffering, no read timeout.
proxy_buffering off;
proxy_read_timeout 86400s;
}
location /websocket {
proxy_pass http://moonraker/websocket;
proxy_http_version 1.1;
+7 -6
View File
@@ -24,8 +24,8 @@ SW_VER=""
# package (and vice versa).
TARGET_MACHINE=Creator5Pro
# Override for one command: MODEL=Creator5 make default
# Build both at once: make release PROFILE=default
# Override for one command: MODEL=Creator5 make build
# Build both at once: make release
# ---- inputs (sources you already have on this machine) ----------------------
# Everything below is relative to this. Point it wherever you keep the
@@ -51,7 +51,7 @@ KLIPPER_FORK="$DESKTOP/klipper"
# The toolchanger mod (klippy-extras + configs + helixscreen db entry) used to
# be a TOOLCHANGE= path to a creator5-toolchange checkout. It now lives in this
# repo under payload/klipper/, so there is nothing to set. BUILD_TOOLCHANGE=0
# in a profile still turns it off.
# here still turns it off.
# Mainsail and the HelixScreen build for the Creator 5 (MIPS static) are NOT
# set here. They are downloaded on demand into vendor/ by bin/fetch-assets.sh,
@@ -69,10 +69,11 @@ 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, BUILD_APPLY, BUILD_REPORT)
# are owned by profiles/*.env and are deliberately not set here: the profile is
# sourced after this file and would override anything you put in.
# 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.
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
MOD_WIFI=1 # 1 = bring up wlan0 (firmwareExe used to do this)
+2
View File
@@ -39,7 +39,9 @@ Services are ordinary init.d-style scripts in `/usr/data/anvil/init.d/`, run in
filename order and individually restartable over ssh:
```
S50wifi wlan0 + wpa_supplicant + udhcpc
S60web nginx (Mainsail) + moonraker
S65camera mjpg-streamer on :8080 (nginx proxies it at /webcam/)
S70klipper Klipper
S80ui decides whether the UI runs; owns SAFE-MODE
```
+31 -15
View File
@@ -55,9 +55,9 @@ Missing: a START_PRINT gate refusing when a requested tool is not docked (`_FF_R
No ML runtime linked (no ncnn/tflite/onnx; OpenCV only for capture), no upload endpoint, no
MQTT verdict topic. The feature is one boolean `aiCheck` in general.json that boots to `false`
(`MainWindow.c:275`); `SettingInfo::doAiDetected()` hard-writes 0. `E0164` is a dead string.
→ nothing to port; Obico off-box (host is MIPS X2000, 128–256 MB, no NPU). Note the `[webcam]`
entry this used to suggest is not available: the `:8080` stream is firmwareExe's own, see
"What is lost with firmwareExe" below.
→ nothing to port; Obico off-box (host is MIPS X2000, 128–256 MB, no NPU) with a `[webcam]`
entry in moonraker.conf -- see "What is lost with firmwareExe" below for how the stream is
served now.
## Corrections to earlier notes
- The "station" used for nozzle offsets is **not** the eddy probe. `[e_stop X|Y|Z]` all sit on
@@ -81,16 +81,32 @@ against it stop working the moment the mod is installed. Moonraker's API replace
(`/server/files/upload`, `/printer/print/start`); tool-to-slot mapping stops being a request
field and becomes the slicer's job, baked into the gcode.
### The camera stream on :8080 -- gone, and not restorable by restarting anything
This one is easy to get wrong, because the URL looks like mjpg-streamer:
`http://<ip>:8080/?action=stream`, `multipart/x-mixed-replace;boundary=boundarydonotcross`.
It is not. `strings firmwareExe` shows that request line, that boundary, `:8080`, and the
binary's own accept/bind/pthread_create error strings alongside `cameraOpen` and
`/usr/data/firmwareRes/camera/*` -- the MJPEG server is compiled into the app, over its OpenCV
capture. There is no mjpg-streamer process on the box to keep alive.
### The camera stream on :8080 -- lost with the app, but restored from the disk
Easy to get wrong in both directions, so both halves matter.
So unlike WiFi -- where firmwareExe merely *drove* stock binaries and `S50wifi` could take over
the same wpa_supplicant -- there is nothing here to take over. Restoring the camera means
shipping a streamer built for MIPS (ustreamer or mjpg-streamer) in the payload and starting it
from an init script, then adding the `[webcam]` block that `assets/moonraker.conf` keeps
commented out. Until then the printer has no camera and Mainsail correctly shows none.
On stock, the stream *is* firmwareExe's: `strings firmwareExe` shows `GET /?action=stream`,
`multipart/x-mixed-replace;boundary=boundarydonotcross`, `:8080`, its own accept/bind/
pthread_create error strings, `cameraOpen` and `/usr/data/firmwareRes/camera/*` -- an HTTP
server compiled into the app over its OpenCV capture, right down to announcing
`Server: MJPG-Streamer/0.2`. Kill firmwareExe and that server goes with it.
But the real thing is also on the box and merely unused. `/usr/prog/mjpg-streamer/` holds
`mjpg_streamer`, `input_uvc.so`, `output_http.so`, its own `libjpeg.so.9` and a `www/` dir, and
`app_startup.sh:159` exports `LD_LIBRARY_PATH=/usr/prog/mjpg-streamer` for it -- and then never
launches it. So nothing has to be cross-compiled or shipped: start FlashForge's own binary.
`payload/init.d/S65camera` does exactly that, on the S50wifi pattern (the wait for `/dev/video0`
and the respawn loop are backgrounded so a printer with no camera never delays the boot):
```sh
export LD_LIBRARY_PATH=/usr/prog/mjpg-streamer:$LD_LIBRARY_PATH
cd /usr/prog/mjpg-streamer
./mjpg_streamer -i "input_uvc.so -d /dev/video0 -r 1280x720 -f 30" \
-o "output_http.so -p 8080 -w www"
```
Two details that bite: `libjpeg.so.9` lives in that directory, so without the LD_LIBRARY_PATH the
binary dies on a missing shared object (the same failure moonrakerDaemon has in S60web); and the
plugin `.so` names resolve against the working directory, hence the `cd`. nginx proxies it at
`/webcam/` and `assets/moonraker.conf` carries the matching `[webcam]` entry with relative URLs,
so one forwarded port is enough.
+8
View File
@@ -6,6 +6,14 @@
# 0 = no web UI at all, ssh only.
MOD_WEB=1
# Camera: mjpg-streamer on :8080, proxied by nginx at /webcam/. The binary is
# FlashForge's own, already on the firmware partition; firmwareExe used to be
# what started it. Optional tunables, defaults shown:
#MOD_CAM_RES=1280x720
#MOD_CAM_FPS=30
#MOD_CAM_PORT=8080
MOD_CAM=1
# SSH: dropbear on :22
MOD_SSH=1
+104
View File
@@ -0,0 +1,104 @@
#!/bin/sh
# Camera -- mjpg-streamer on :8080.
#
# WHY THIS EXISTS -- the same trap as WiFi. /usr/prog/mjpg-streamer ships on
# stock firmware (mjpg_streamer, input_uvc.so, output_http.so, its own
# libjpeg.so.9 and a www/ dir), and app_startup.sh only ever exports
# LD_LIBRARY_PATH for it -- it never launches it. firmwareExe did, and in fact
# served the stream from its own OpenCV capture, labelling itself
# "Server: MJPG-Streamer/0.2". So the moment the mod replaces firmwareExe the
# camera goes dark, with a perfectly good streamer sitting unused on the disk.
#
# Nothing has to be built or shipped for this: we start the binary FlashForge
# already put there. Mainsail then finds it through the [webcam] entry in
# moonraker.conf, which points at nginx's /webcam/ proxy rather than :8080
# directly, so a tunnel that forwards one port still shows the stream.
#
# LD_LIBRARY_PATH matters: libjpeg.so.9 lives in the mjpg-streamer directory
# itself, so without it the binary dies on a missing shared object -- the same
# failure moonrakerDaemon has in S60web. The plugin names are resolved relative
# to the working directory, hence the cd.
#
# LIKE S50wifi, IT MUST NOT MAKE THE BOOT WAIT. The USB camera can take a few
# seconds to enumerate, and a printer with no camera plugged in would otherwise
# hold up Klipper and the UI for the full timeout. The wait and the respawn
# loop live in a background subshell; start() returns at once.
MODDIR=/usr/data/anvil
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
CAMDIR=/usr/prog/mjpg-streamer
VIDEO=/dev/video0
PIDFILE=$MODDIR/mjpg.pid
LOG=/usr/data/logs/mjpg.log
RES="${MOD_CAM_RES:-1280x720}"
FPS="${MOD_CAM_FPS:-30}"
PORT="${MOD_CAM_PORT:-8080}"
streamer_running() {
ps 2>/dev/null | grep mjpg_streamer | grep -v grep >/dev/null 2>&1
}
# The part that waits. Detached from the boot sequence, so nothing reads its
# exit code -- it reports through the boot log only.
supervise() {
i=0
while [ ! -e "$VIDEO" ] && [ $i -lt 30 ]; do
sleep 1
i=$((i + 1))
done
if [ ! -e "$VIDEO" ]; then
echo "camera: $VIDEO never appeared -- no camera plugged in?"
return 1
fi
mkdir -p /usr/data/logs
export LD_LIBRARY_PATH=$CAMDIR:$LD_LIBRARY_PATH
cd "$CAMDIR" || return 1
# Respawn: a USB camera that is unplugged and replugged takes the streamer
# down with it, and there is no other supervisor on this box.
while true; do
./mjpg_streamer \
-i "input_uvc.so -d $VIDEO -r $RES -f $FPS" \
-o "output_http.so -p $PORT -w www" \
>>"$LOG" 2>&1
sleep 3
done
}
start() {
if [ "${MOD_CAM:-1}" != "1" ]; then
echo "camera: disabled by MOD_CAM"
return 0
fi
[ -x "$CAMDIR/mjpg_streamer" ] || { echo "camera: no $CAMDIR/mjpg_streamer"; return 1; }
if streamer_running; then
echo "camera: mjpg_streamer already running"
return 0
fi
supervise &
echo $! > "$PIDFILE"
echo "camera: waiting for $VIDEO in the background (:$PORT, $RES @ ${FPS}fps)"
return 0
}
stop() {
# The supervisor first: kill only the streamer and it is restarted in 3s.
[ -f "$PIDFILE" ] && kill `cat "$PIDFILE"` 2>/dev/null
rm -f "$PIDFILE"
killall mjpg_streamer 2>/dev/null
echo "camera: stopped"
}
case "$1" in
start|'') start ;;
stop) stop ;;
restart) stop; sleep 2; start ;;
status)
streamer_running && echo "camera: mjpg_streamer running on :$PORT" \
|| echo "camera: not running"
[ -e "$VIDEO" ] && echo "camera: $VIDEO present" || echo "camera: no $VIDEO"
;;
*) echo "usage: $0 {start|stop|restart|status}"; exit 1 ;;
esac