From 607e6595fb5341e544a93085b66090f4e9cccea8 Mon Sep 17 00:00:00 2001 From: Oleksandr Shyshatskyi Date: Sun, 23 Aug 2026 22:06:25 +0300 Subject: [PATCH] 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) --- README.md | 36 ++++---- assets/moonraker.conf | 27 +++--- assets/nginx.conf | 12 +++ config.env.example | 13 +-- docs/how-it-works.md | 2 + docs/notes/25-app-vs-klipper-ownership.md | 46 ++++++---- payload/anvil.conf | 8 ++ payload/init.d/S65camera | 104 ++++++++++++++++++++++ 8 files changed, 195 insertions(+), 53 deletions(-) create mode 100644 payload/init.d/S65camera diff --git a/README.md b/README.md index eff3e05..228ee4c 100644 --- a/README.md +++ b/README.md @@ -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:///` 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. --- diff --git a/assets/moonraker.conf b/assets/moonraker.conf index b86421a..cba2358 100644 --- a/assets/moonraker.conf +++ b/assets/moonraker.conf @@ -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 diff --git a/assets/nginx.conf b/assets/nginx.conf index b1d6e2d..e1ca8f7 100644 --- a/assets/nginx.conf +++ b/assets/nginx.conf @@ -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; diff --git a/config.env.example b/config.env.example index a5355b5..86ab575 100644 --- a/config.env.example +++ b/config.env.example @@ -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) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index ef539f1..8a04282 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -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 ``` diff --git a/docs/notes/25-app-vs-klipper-ownership.md b/docs/notes/25-app-vs-klipper-ownership.md index 8401c5f..408eaff 100644 --- a/docs/notes/25-app-vs-klipper-ownership.md +++ b/docs/notes/25-app-vs-klipper-ownership.md @@ -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://: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. diff --git a/payload/anvil.conf b/payload/anvil.conf index f4cde7b..d24f0fb 100644 --- a/payload/anvil.conf +++ b/payload/anvil.conf @@ -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 diff --git a/payload/init.d/S65camera b/payload/init.d/S65camera new file mode 100644 index 0000000..5061f1f --- /dev/null +++ b/payload/init.d/S65camera @@ -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