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