diff --git a/bin/patch.sh b/bin/patch.sh index 7c2c370..58e84a8 100755 --- a/bin/patch.sh +++ b/bin/patch.sh @@ -176,18 +176,6 @@ case "$S6RC_SHEBANG" in esac say "s6-rc: database $S6RC_DB_NAME compiled -- $(ls "$S6RC_SRC" | wc -l) definitions" -# --- 5c. CPython 3.13 (shipped) -# FF_PYTHON points here -- but not for klippy, which runs on the stock 3.8.2. -# That 3.8.2 has no _sqlite3, which is what pins MOONRAKER_VERSION to 2023. -# The dev half is never installed, so verify.sh's "no headers" check passes. - -# Each python-* recipe asserts its own extension: an aggregate count cannot say -# WHICH one fell back to a pure-python wheel. A gate in build.sh does not run -# on a cache hit. -du -sh "$PAYLOAD_DIR/lib/python$PY_MM" | awk '{print " "$1"\tlib/python'"$PY_MM"'/"}' -du -sh "$PAYLOAD_DIR/lib/python$PY_MM/site-packages" \ - | awk '{print " "$1"\tlib/python'"$PY_MM"'/site-packages/"}' - # --- 6. SSH # Nothing to install: the stock rootfs ships dropbear and an enabled # S50dropbear, so ssh is already listening. Only the password is missing. @@ -209,23 +197,17 @@ else skip "root password (set ROOT_PW_HASH)" fi -# --- 8. start.sh (web stack on) -# From the installed payload, but still carried in the component: that is what -# FlashForge's run.sh copies to /usr/prog, and what keeps a printer bootable -# when the payload is not there. anvil-link-prog.sh then swaps it for a link. -say "start.sh: enabling nginx + moonraker" -cp -f "$PAYLOAD_DIR/prog/start.sh" "$SOFTWARE_DIR/start.sh" -chmod +x "$SOFTWARE_DIR/start.sh" - -# --- 9. firmwareExe -> our wrapper script -# The stock chain is rcS -> S99factory_test_shell -> app_startup.sh -> -# firmwareExe, which also starts Klipper: this one file owns the whole -# userspace boot and leaves the init chain stock. The binary is not kept -# aside -- flashing the stock package is the uninstall. From the payload, as -# in section 8. -say "firmwareExe: installing wrapper (replaces the stock binary)" -cp -f "$PAYLOAD_DIR/prog/firmwareExe" "$SOFTWARE_DIR/firmwareExe" -chmod +x "$SOFTWARE_DIR/firmwareExe" +# --- 8. firmwareExe + start.sh -- anvil-core's, and NOT in the component +# Both install at $MODDIR/prog/ and anvil-link-prog.sh points the stock paths +# at them, on the flash path (run-append.sh) and on `opkg upgrade anvil-core` +# (postinst). Nothing execs either between run.sh copying the component and +# link-prog running, so the component copy bought no ordering -- and it cost: +# our wrapper with no payload never starts a UI, it logs and sleeps for ever, +# turning a failed install into an unbootable printer. Absent, app_startup.sh +# restores a firmwareExe from a version directory and the stock recovery works. +# Stock run.sh is set -x, not set -e, so its two cp lines are noisy no-ops. +say "firmwareExe + start.sh: dropped from the component (anvil-core owns both)" +rm -f "$SOFTWARE_DIR/firmwareExe" "$SOFTWARE_DIR/start.sh" # --- 10. anvil.conf # anvil.conf is not in anvil-core and is the whole of what is left here: diff --git a/bin/verify.sh b/bin/verify.sh index 86319cf..7ce4edd 100755 --- a/bin/verify.sh +++ b/bin/verify.sh @@ -62,7 +62,9 @@ if [ -n "$SOFTWARE_TAR" ]; then fi # --- 5. shell syntax of scripts -for s in "$WORK/runFirmwareExe.sh" "$WORK/sw/run.sh" "$WORK/sw/start.sh" "$WORK/sw/app_startup.sh"; do +# start.sh and firmwareExe are the payload's now, not the component's -- section +# 7 extracts and checks them there. +for s in "$WORK/runFirmwareExe.sh" "$WORK/sw/run.sh" "$WORK/sw/app_startup.sh"; do [ -f "$s" ] || continue if sh -n "$s" 2>/dev/null; then ok "syntax OK: $(basename "$s")" else bad "SYNTAX ERROR in $(basename "$s")"; sh -n "$s" 2>&1 | head -3 | sed 's/^/ /'; fi @@ -84,14 +86,20 @@ fi # chelper.tar that carried its .so are both gone. Unconditional, because # v20260824 passed here by having every check below conditional on files the # broken build did not contain. ./klipper alone, not the whole payload -- -# section 8 answers the rest from a listing. Paths carry a leading ./ because -# bin/pack.sh tars `.` from inside $PAYLOAD_DIR. +# section 8 answers the rest from a listing. ./prog rides along because the two +# stock-path scripts moved there and still need a syntax gate. Paths carry a +# leading ./ because bin/pack.sh tars `.` from inside $PAYLOAD_DIR. PAYLOAD_TAR=$(ls -1 "$WORK"/anvil.tar.xz 2>/dev/null | head -n1) if [ -n "$PAYLOAD_TAR" ]; then mkdir -p "$WORK/pl" - xz -dc "$PAYLOAD_TAR" 2>/dev/null | tar -xf - -C "$WORK/pl" ./klipper 2>/dev/null \ - || tar -xf "$PAYLOAD_TAR" -C "$WORK/pl" ./klipper 2>/dev/null + xz -dc "$PAYLOAD_TAR" 2>/dev/null | tar -xf - -C "$WORK/pl" ./klipper ./prog 2>/dev/null \ + || tar -xf "$PAYLOAD_TAR" -C "$WORK/pl" ./klipper ./prog 2>/dev/null fi +for s in "$WORK/pl/prog/start.sh" "$WORK/pl/prog/firmwareExe"; do + [ -f "$s" ] || { bad "the payload carries no prog/$(basename "$s") -- link-prog has nothing to point the stock path at"; continue; } + if sh -n "$s" 2>/dev/null; then ok "syntax OK: prog/$(basename "$s")" + else bad "SYNTAX ERROR in prog/$(basename "$s")"; sh -n "$s" 2>&1 | head -3 | sed 's/^/ /'; fi +done if [ -f "$WORK/pl/klipper/klippy/chelper/__init__.py" ]; then ok "payload carries the fork klippy tree" else @@ -125,6 +133,15 @@ fi [ -e "$WORK/sw/klipper/klippy" ] \ && bad "the software component still carries a klippy tree -- bin/patch.sh section 1 is staging one again" \ || ok "no klippy tree in the software component (the payload is the only one)" +# Same rule for the two stock-path scripts: anvil-core owns them at +# $MODDIR/prog/ and link-prog symlinks them into place. A component copy of the +# wrapper is worse than none -- with no payload it never starts a UI, where an +# absent one lets app_startup.sh restore the stock binary. +for f in firmwareExe start.sh; do + [ -e "$WORK/sw/$f" ] \ + && bad "the software component still carries $f -- bin/patch.sh section 8 is staging one again" \ + || ok "no $f in the software component (anvil-core's \$MODDIR/prog is the only one)" +done # --- 8. mod payload if [ -f "$WORK/anvil.tar.xz" ]; then diff --git a/docs/building.md b/docs/building.md index 5f84693..62717fb 100644 --- a/docs/building.md +++ b/docs/building.md @@ -233,12 +233,11 @@ Which is to say: ``` pkgs/anvil-core/ what makes the machine ours, and nothing else payload/ anvil-env.sh PATH/LD_LIBRARY_PATH/FF_PYTHON -- sourced - anvil-service.sh the start/stop/status/liveness shape services share - init.d/ S40s6, S50wifi, S60nginx, S62moonraker, S65camera, - S70klipper, S80ui bin/ ff-startup.py, ff_mcu_bringup.py, ffscreen.py, wifi-action.sh - etc/s6/ the s6 scandir, one directory per service + etc/s6-rc/source/ one directory per service: wifi, nginx, moonraker, + camera, klipper, ui, mcu-bringup, ff-startup, + ok-all nginx/nginx.conf bin/anvil-link-prog.sh links the two below, and the configs, into the absolute paths the stock scripts read diff --git a/docs/how-a-print-runs.md b/docs/how-a-print-runs.md index 164c521..8afdfda 100644 --- a/docs/how-a-print-runs.md +++ b/docs/how-a-print-runs.md @@ -125,7 +125,7 @@ START_PRINT TOOLS=0:220,2:240 BED=60 LEVEL=1 SOAK=300 The macros are on the printer at `/usr/data/config/ff-print-macros.cfg`, and in this repo at -[`payload/klipper/config/ff-print-macros.cfg`](../payload/klipper/config/ff-print-macros.cfg). +[`pkgs/klipper-config/payload/config/ff-print-macros.cfg`](../pkgs/klipper-config/payload/config/ff-print-macros.cfg). They are ordinary Klipper macros with comments explaining why each step is what it is. diff --git a/docs/how-it-works.md b/docs/how-it-works.md index f906541..f77ccc2 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -130,9 +130,9 @@ in `anvil-env.sh` names a CPython 3.13 of our own, cross-built for mipsel by recipe and one `.ipk` each — and libsodium in `$MODDIR/lib`; none of it borrowed from `/usr/prog`. Measured through the real boot path (the scandir, the moonraker service, readiness gating on `:7125` -actually listening, a `kill -9` respawn, a stop that stays stopped). klippy is not part of -this: it stays on FlashForge's 3.8.2, started independently by -`/usr/prog/klipper/start.sh`. +actually listening, a `kill -9` respawn, a stop that stays stopped). klippy is on it +too: the `klipper` service execs `$FF_PYTHON` against the klippy tree in +`$MODDIR/klipper`, so both halves are ours. **The Moonraker commit pin is still a 2023 one, though the reason it exists has changed.** FlashForge built python 3.8.2 without the `_sqlite3` module, @@ -163,10 +163,10 @@ 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, that the -shipped tree runs on the printer's own python3.8 **and is still running after a -settle**, and that its log has no import errors. The settle matters — a build -that dies on a missing module is alive for a second or two first, and checking -only "did it start" is what let the sqlite3 failure through. +shipped tree runs on our own CPython 3.13 (`$FF_PYTHON`) **and is still running +after a settle**, and that its log has no import errors. The settle matters — a +build that dies on a missing module is alive for a second or two first, and +checking only "did it start" is what let the sqlite3 failure through. `BUILD_MOONRAKER=0` builds a package that leaves the stock server alone. ## Five things the stock firmware does that will surprise you diff --git a/docs/notes/25-app-vs-klipper-ownership.md b/docs/notes/25-app-vs-klipper-ownership.md index 15d3f20..8b99df0 100644 --- a/docs/notes/25-app-vs-klipper-ownership.md +++ b/docs/notes/25-app-vs-klipper-ownership.md @@ -9,9 +9,9 @@ print is driven from Mainsail unless `ff_*` re-provides it. |---|---|---|---| | Heaters | `M104 S.. T0..T3`, `M140`, `SET_HEATER_TEMPERATURE HEATER=chamber_heater`; **never M109/M190** — waiting is app-side polling; staged heating e1+e2 then e3+e4; 5-min bed soak; chamber heater coupled to `chamber_heat_fan` 0.9 / `chamber_loop_fan` 0.3 in the app only | `[verify_heater]` (fork hardcodes gain times, caps concurrent extruder heating at 2, loosens PID settle so M109 returns early); `min_extrude_temp: -200` on all extruders = cold-extrude guard effectively off. The chamber is **ported**: `ff-chamber.cfg` wraps `SET_HEATER_TEMPERATURE` and adds `M141`/`M191`, gated on the model that actually has an element | `CommMgr::heatManager` @0x76… (CommMgr.c:2576-2700) | | Material temps | from `filament.json` via `getFilamentOperationTemp` | none | CommMgr.c, LoadFilamentPrint.c | -| Runout / clog | app state machine on `filament_switch_sensor fd_ex*` `filament_detected` + `"wheel runout:Tn"` text; arms/disarms with `SET_FILAMENT_SENSOR … ENABLE=0/1`; endless-spool auto-swap (E0162/E0163) | all eight sensors `pause_on_runout: False`; motion sensors' `runout_gcode` only `action_respond_info` — **ported** (mounted-tool arming in `ff_toolchange.py`, pause via `pkgs/anvil-core/payload/config/ff-runout.cfg`; no endless spool), see `49-runout-recovered.md` | `printer.filament.cfg`, `dealFilamentWheelStatus`, `checkAndAutoFeed` | +| Runout / clog | app state machine on `filament_switch_sensor fd_ex*` `filament_detected` + `"wheel runout:Tn"` text; arms/disarms with `SET_FILAMENT_SENSOR … ENABLE=0/1`; endless-spool auto-swap (E0162/E0163) | all eight sensors `pause_on_runout: False`; motion sensors' `runout_gcode` only `action_respond_info` — **ported** (mounted-tool arming in `ff_toolchange.py`, pause via `pkgs/klipper-config/payload/config/ff-runout.cfg`; no endless spool), see `49-runout-recovered.md` | `printer.filament.cfg`, `dealFilamentWheelStatus`, `checkAndAutoFeed` | | Doors | `openDoorPause` toggle in general.json | `[gcode_button topDoor/frontDoor]` with **empty** `press_gcode` | `printer.base.cfg:278-287` | -| Load / unload / purge | app sequences (`FilamentLoad::doLoad/doUnload`, `clearNozzlePrint`) — grab tool, purge chute X275 Y254, material+30 °C, `G1 E150`+`E145`; "unload" is the same push | nothing (extruders are direct drive; `gear_stepper` is the tool lock) — **ported** to `pkgs/anvil-core/payload/config/ff-filament.cfg`, see `48-filament-load-port.md` | `47-filament-load-recovered.md` | +| Load / unload / purge | app sequences (`FilamentLoad::doLoad/doUnload`, `clearNozzlePrint`) — grab tool, purge chute X275 Y254, material+30 °C, `G1 E150`+`E145`; "unload" is the same push | nothing (extruders are direct drive; `gear_stepper` is the tool lock) — **ported** to `pkgs/klipper-config/payload/config/ff-filament.cfg`, see `48-filament-load-port.md` | `47-filament-load-recovered.md` | | Print lifecycle | prepare (14 steps), preamble, pause (**all hotends off**), resume (staged reheat + re-grab), exit block | no START/END/PAUSE/RESUME macros in stock config | 50-print-lifecycle.md | | Z frame | absolute print-start Z offset (~+3.2 mm) computed in `BuildPage::startPrint`; per-tool XY/Z diffs applied on every grab | nothing; eddy `G28 Z` is **not** nozzle zero | 40-offsets.md | | Mesh / leveling | app triggers `BED_MESH_CALIBRATE` / `BED_MESH_PROFILE LOAD=…` | executes | 50-print-lifecycle.md | @@ -117,5 +117,5 @@ Two details that bite: `libjpeg.so.9` lives in that directory, so without the LD binary dies on a missing shared object (the same failure moonrakerDaemon had, which is why `anvil-env.sh` is the one place the library path is written down); and the plugin `.so` names resolve against the working directory, hence the `cd`. nginx proxies it at -`/webcam/` and `pkgs/anvil-core/payload/config/moonraker.conf` carries the matching `[webcam]` entry with relative URLs, +`/webcam/` and `pkgs/moonraker/payload/config/moonraker.conf` carries the matching `[webcam]` entry with relative URLs, so one forwarded port is enough. diff --git a/docs/notes/45-tool-offset-calibration.md b/docs/notes/45-tool-offset-calibration.md index cbe540d..c8e58d1 100644 --- a/docs/notes/45-tool-offset-calibration.md +++ b/docs/notes/45-tool-offset-calibration.md @@ -16,8 +16,8 @@ per-tool sections, modelled on `[bed_mesh ]` / klipper-toolchanger's `[ | `pkgs/klipper/payload/klipper/klippy/extras/ff_tool_offset.py` | `TOOL_CALIBRATE_TOOL_OFFSET`, `TOOL_LOCATE_SENSOR`, `TOOL_OFFSET_STATUS`; `[ff_tool_offset]` holds `station_x/y/z` (autosaved) + probe geometry | | `pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py` | takes docks/offsets from the `ff_tool` objects; `refresh_offsets()`; `_station_z()`; JSON reader and `TOOLCHANGE_RELOAD` removed | | `pkgs/klipper/payload/klipper/klippy/extras/ff_legacy.py` | `FF_IMPORT_FIRMWARE_CONFIG [DIR=] [APPLY=1]` — one-shot import of extruder/test/zoffset.json | -| `pkgs/anvil-core/payload/config/ff-toolchange.cfg` | four deliberately empty `[ff_tool 0..3]` sections + `[ff_toolchange]`. Per-unit values must NOT go here — `SAVE_CONFIG` refuses to autosave an option an include already sets | -| `pkgs/anvil-core/payload/config/ff-tool-offset.cfg`, `pkgs/anvil-core/payload/config/ff-legacy.cfg` | sections for the other two extras | +| `pkgs/klipper-config/payload/config/ff-toolchange.cfg` | four deliberately empty `[ff_tool 0..3]` sections + `[ff_toolchange]`. Per-unit values must NOT go here — `SAVE_CONFIG` refuses to autosave an option an include already sets | +| `pkgs/klipper-config/payload/config/ff-tool-offset.cfg`, `pkgs/klipper-config/payload/config/ff-legacy.cfg` | sections for the other two extras | ## Storage layout @@ -57,7 +57,7 @@ A built package needs none of this — it installs the extras and hand, onto a printer that is already modded: ``` -cp pkgs/klipper/payload/klipper/klippy/extras/ff_*.py /usr/prog/klipper/klippy/extras/ +cp pkgs/klipper/payload/klipper/klippy/extras/ff_*.py /usr/data/anvil/klipper/klippy/extras/ # Nothing to add to printer.cfg: printer.base.cfg already includes the # ff-*.cfg set. Section order does not matter. RESTART diff --git a/docs/notes/48-filament-load-port.md b/docs/notes/48-filament-load-port.md index 33fcfb4..ddc5d85 100644 --- a/docs/notes/48-filament-load-port.md +++ b/docs/notes/48-filament-load-port.md @@ -2,7 +2,7 @@ Source of truth: [`47-filament-load-recovered.md`](47-filament-load-recovered.md), recovered 2026-08-21 from MIPS code (Ghidra bodies were 36-byte stubs). -Port: `pkgs/anvil-core/payload/config/ff-filament.cfg` — `LOAD_FILAMENT`, `UNLOAD_FILAMENT`, `PURGE`, tunables in +Port: `pkgs/klipper-config/payload/config/ff-filament.cfg` — `LOAD_FILAMENT`, `UNLOAD_FILAMENT`, `PURGE`, tunables in `[gcode_macro _FF_FILAMENT]`. ## What the app really does diff --git a/docs/notes/49-runout-recovered.md b/docs/notes/49-runout-recovered.md index 4c9b1a4..3fb5969 100644 --- a/docs/notes/49-runout-recovered.md +++ b/docs/notes/49-runout-recovered.md @@ -87,7 +87,7 @@ error): * `FF_RUNOUT_ARM [TOOL=]` / `FF_RUNOUT_DISARM`; status `runout_armed` (tool or −1), `runout_sensors` (object names); `TOOLCHANGE_STATUS` prints the same. -`pkgs/anvil-core/payload/config/ff-runout.cfg` (include after `printer.base.cfg`; Klipper merges repeated sections, +`pkgs/klipper-config/payload/config/ff-runout.cfg` (include after `printer.base.cfg`; Klipper merges repeated sections, later options win — the stock `printer.filament.cfg` is left untouched): * the eight sections get `runout_gcode: _FF_RUNOUT TOOL=n KIND=switch|motion` and diff --git a/docs/notes/50-print-lifecycle.md b/docs/notes/50-print-lifecycle.md index 9c4776d..5e9778e 100644 --- a/docs/notes/50-print-lifecycle.md +++ b/docs/notes/50-print-lifecycle.md @@ -1,7 +1,7 @@ # Print lifecycle, address-verified Everything below lives in the APP, not in Klipper: the fork ships only bare -`[pause_resume]` and `[virtual_sdcard]`. `pkgs/anvil-core/payload/config/ff-print-macros.cfg` reproduces the +`[pause_resume]` and `[virtual_sdcard]`. `pkgs/klipper-config/payload/config/ff-print-macros.cfg` reproduces the sequences for Mainsail-started prints. **How they are entered now.** `START_PRINT` is no longer something the slicer has to diff --git a/docs/notes/50a-nozzle-clean-recovered.md b/docs/notes/50a-nozzle-clean-recovered.md index 5523821..8760f50 100644 --- a/docs/notes/50a-nozzle-clean-recovered.md +++ b/docs/notes/50a-nozzle-clean-recovered.md @@ -3,7 +3,7 @@ Recovered 2026-08-22 from the MIPS code of `BuildPage::clearNozzlePrint` @0x9f46f4, `clearNozzleCheckTemp` @0x9f6ce0, `clearNozzleProbez` @0x9f4020 and the presence check in `prepareForEddy` @0x9f1068 (the Ghidra bodies are 36-byte stubs, cut at the first `ErrorBak` -constructor). Port: `_FF_NOZZLE_CLEAN` / `_FF_PREFLIGHT` in `pkgs/anvil-core/payload/config/ff-print-macros.cfg`, +constructor). Port: `_FF_NOZZLE_CLEAN` / `_FF_PREFLIGHT` in `pkgs/klipper-config/payload/config/ff-print-macros.cfg`, see [`50b-nozzle-clean-port.md`](50b-nozzle-clean-port.md). ## Where it sits in the prepare flow (prepareForEddy) diff --git a/docs/notes/50b-nozzle-clean-port.md b/docs/notes/50b-nozzle-clean-port.md index 6f79ddd..ef08dec 100644 --- a/docs/notes/50b-nozzle-clean-port.md +++ b/docs/notes/50b-nozzle-clean-port.md @@ -1,9 +1,9 @@ # Pre-print nozzle clean and tool gate — Klipper port Source of truth: [`50a-nozzle-clean-recovered.md`](50a-nozzle-clean-recovered.md). -Port: `pkgs/anvil-core/payload/config/ff-print-macros.cfg` — `_FF_PREFLIGHT`, `_FF_NOZZLE_CLEAN`, and the +Port: `pkgs/klipper-config/payload/config/ff-print-macros.cfg` — `_FF_PREFLIGHT`, `_FF_NOZZLE_CLEAN`, and the `TOOLS=` / `TEMPS=` / `CLEAN=` / `SOAK=` parameters of `START_PRINT`; tunables -`clean_*` in `[gcode_macro _FF_FILAMENT]` (`pkgs/anvil-core/payload/config/ff-filament.cfg`); +`clean_*` in `[gcode_macro _FF_FILAMENT]` (`pkgs/klipper-config/payload/config/ff-filament.cfg`); `docked_tools` in `printer.ff_toolchange` status. ## START_PRINT now diff --git a/docs/notes/80-s6-migration.md b/docs/notes/80-s6-migration.md index deb0620..20ef3de 100644 --- a/docs/notes/80-s6-migration.md +++ b/docs/notes/80-s6-migration.md @@ -63,6 +63,8 @@ Get them wrong and the phase fails on a printer, not in CI. * We do **not** install Klipper. `bin/patch.sh` stages a *software component* and FlashForge's own stock `run.sh` copies it onto `/usr/prog/klipper`. That is why Klipper sits on the firmware partition -- inherited, not chosen. + **Superseded by phase 7:** `anvil-klipper` installs the tree at + `$MODDIR/klipper` and the `klipper` service runs it from there. * `pkgs/klipper/prog/start.sh` already **is** `/usr/prog/klipper/start.sh`; the mod replaces it. What we do not own are the FlashForge pieces it calls: `klipperDaemon`, `klipper_pri.sh`, `checkEboard`, `libmcu-bare.bin`. @@ -504,7 +506,9 @@ So s6-rc roughly DOUBLES what supervision costs on the data partition, and it does so to buy a boot order that is three real edges wide (see the section above) and was previously spelled as filenames. That is the honest trade and it should be read next to the free-space unknown phase 6 records: nobody has ever run `df` on a real printer. -`s6-rc-compile` (131KB) is host-side only and is not shipped. +`s6-rc-compile` (131KB) is host-side only and is not shipped. [Superseded: it +ships. `pkgs/3rdparty/s6-rc/build.sh` keeps it deliberately -- it is the only +way to fix a printer whose database is wrong without a laptop.] **Both new packages must be configured `--prefix=/usr/data/anvil`, for a reason one level worse than the `s6-ftrigrd` trap.** execline's `--shebangdir` diff --git a/docs/notes/85-packaging.md b/docs/notes/85-packaging.md index 998e01f..216a50f 100644 --- a/docs/notes/85-packaging.md +++ b/docs/notes/85-packaging.md @@ -373,7 +373,9 @@ The package installs the tree under `$MODDIR/klipper`, where nothing reads it yet: klippy is still started by the stock `/usr/prog/klipper/klipperDaemon`, so `bin/patch.sh` stages the recipe's output into the SOFTWARE component as before. Phase 7 of `docs/notes/80-s6-migration.md` is what makes the installed -copy the live one. +copy the live one. [Superseded: phase 7 landed. The `klipper` s6-rc service +execs `$FF_PYTHON` against `$MODDIR/klipper/klippy`, and the software-component +copy is gone.] `patch.sh` keeps staging the payload from those same trees, so the shipped tarball does not change. The work is mechanical; the risk is in the two builds diff --git a/docs/qa-migration.md b/docs/qa-migration.md index 97a0b5d..a19402e 100644 --- a/docs/qa-migration.md +++ b/docs/qa-migration.md @@ -476,8 +476,9 @@ again, and should know two things it had learnt. `make_fixture` staged into mounts host-side, so a path under the build container's `/tmp` does not exist as far as the daemon is concerned. And the shape it reproduced -- the software component's `run.sh` ending in `tar -xf -$WORK_DIR/klipper/chelper.tar` -- is what `bin/patch.sh` section 4 still -depends on. Recover both from git history rather than from a fresh reading of +$WORK_DIR/klipper/chelper.tar` -- is what `bin/patch.sh` depended on while the +klippy tree still went out on the firmware partition; the tree lives at +`$MODDIR/klipper` now and neither that section nor the `chelper.tar` exists. Recover both from git history rather than from a fresh reading of a stock package. ### What survives in test/ diff --git a/docs/toolchange.md b/docs/toolchange.md index 2e99c60..0a505d1 100644 --- a/docs/toolchange.md +++ b/docs/toolchange.md @@ -19,23 +19,23 @@ documented in the file headers. | File | Goes to (on the printer) | What it does | |---|---|---| -| [`pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py) | `/usr/prog/klipper/klippy/extras/` | The toolchanger: `T0..T3`, dock/grab state machine with sensor polling and retries, per-tool G-code offsets (the absolute ~3.2 mm bed-frame Z is applied at every grab), `TOOLCHANGE_SET_PRINT_OFFSET` (the print-start thermal/bed/layer Z terms), `TOOL_Z_ADJUST` (per-tool babystep, live, saved on request), `TOOLCHANGE_STATUS`, `TOOLCHANGE_PARK` | +| [`pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py) | `/usr/data/anvil/klipper/klippy/extras/` | The toolchanger: `T0..T3`, dock/grab state machine with sensor polling and retries, per-tool G-code offsets (the absolute ~3.2 mm bed-frame Z is applied at every grab), `TOOLCHANGE_SET_PRINT_OFFSET` (the print-start thermal/bed/layer Z terms), `TOOL_Z_ADJUST` (per-tool babystep, live, saved on request), `TOOLCHANGE_STATUS`, `TOOLCHANGE_PARK` | | [`pkgs/helixscreen/payload/helixscreen/config/printer_database.d/flashforge_creator5.json`](../pkgs/helixscreen/payload/helixscreen/config/printer_database.d/flashforge_creator5.json) | HelixScreen `config/printer_database.d/` | Printer-database entry so HelixScreen auto-detects both Creator 5 models as tool changers (the Pro and the non-Pro differ only by the chamber heater) | -| [`pkgs/klipper/payload/klipper/klippy/extras/ff_print.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_print.py) | `/usr/prog/klipper/klippy/extras/` | `[ff_print]` — takes over `SDCARD_PRINT_FILE` and `M23`, reads bed/nozzle/initial tool/first-layer height out of the file itself, and calls `FF_BEFORE_PRINT_START` before the file's first line and `FF_AFTER_PRINT_END` once the job leaves the printing state. Declared in `ff-print-macros.cfg`; holds no policy of its own | -| [`pkgs/klipper/payload/klipper/klippy/extras/ff_tool.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_tool.py) | `/usr/prog/klipper/klippy/extras/` | `[ff_tool n]` — one section per tool; `dock_x/dock_y`, `nozzle_x/y/z` and `z_adjust` are all autosaved (import or calibration + `SAVE_CONFIG`) | -| [`pkgs/klipper/payload/klipper/klippy/extras/ff_tool_offset.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_tool_offset.py) | `/usr/prog/klipper/klippy/extras/` | `TOOL_CALIBRATE_TOOL_OFFSET` / `TOOL_LOCATE_SENSOR` / `TOOL_OFFSET_STATUS` — the touchscreen's nozzle XY/Z offset calibration, recovered from the binary and reimplemented in Klipper | -| [`pkgs/klipper/payload/klipper/klippy/extras/ff_legacy.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_legacy.py) | `/usr/prog/klipper/klippy/extras/` | `FF_IMPORT_FIRMWARE_CONFIG` — one-shot import of the factory/touchscreen JSON into Klipper config. The command and nothing else: no startup behaviour. `bin/ff-startup.py` is what runs it on the first boot | -| [`pkgs/anvil-core/payload/config/ff-toolchange.cfg`](../pkgs/anvil-core/payload/config/ff-toolchange.cfg) | `/usr/data/config/` | empty `[ff_tool 0..3]` sections (the per-unit dock/nozzle data is autosaved, nothing unit-specific ships), `[ff_toolchange]` feeds/geometry, the `G28` dock-first wrapper | -| [`pkgs/anvil-core/payload/config/ff-tool-offset.cfg`](../pkgs/anvil-core/payload/config/ff-tool-offset.cfg) | `/usr/data/config/` | `[ff_tool_offset]` — probe geometry and guards for the calibration commands | -| [`pkgs/anvil-core/payload/config/ff-legacy.cfg`](../pkgs/anvil-core/payload/config/ff-legacy.cfg) | `/usr/data/config/` | `[ff_legacy]` — stays included permanently; declares the section and, optionally, `firmware_config_dir` | +| [`pkgs/klipper/payload/klipper/klippy/extras/ff_print.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_print.py) | `/usr/data/anvil/klipper/klippy/extras/` | `[ff_print]` — takes over `SDCARD_PRINT_FILE` and `M23`, reads bed/nozzle/initial tool/first-layer height out of the file itself, and calls `FF_BEFORE_PRINT_START` before the file's first line and `FF_AFTER_PRINT_END` once the job leaves the printing state. Declared in `ff-print-macros.cfg`; holds no policy of its own | +| [`pkgs/klipper/payload/klipper/klippy/extras/ff_tool.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_tool.py) | `/usr/data/anvil/klipper/klippy/extras/` | `[ff_tool n]` — one section per tool; `dock_x/dock_y`, `nozzle_x/y/z` and `z_adjust` are all autosaved (import or calibration + `SAVE_CONFIG`) | +| [`pkgs/klipper/payload/klipper/klippy/extras/ff_tool_offset.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_tool_offset.py) | `/usr/data/anvil/klipper/klippy/extras/` | `TOOL_CALIBRATE_TOOL_OFFSET` / `TOOL_LOCATE_SENSOR` / `TOOL_OFFSET_STATUS` — the touchscreen's nozzle XY/Z offset calibration, recovered from the binary and reimplemented in Klipper | +| [`pkgs/klipper/payload/klipper/klippy/extras/ff_legacy.py`](../pkgs/klipper/payload/klipper/klippy/extras/ff_legacy.py) | `/usr/data/anvil/klipper/klippy/extras/` | `FF_IMPORT_FIRMWARE_CONFIG` — one-shot import of the factory/touchscreen JSON into Klipper config. The command and nothing else: no startup behaviour. `bin/ff-startup.py` is what runs it on the first boot | +| [`pkgs/klipper-config/payload/config/ff-toolchange.cfg`](../pkgs/klipper-config/payload/config/ff-toolchange.cfg) | `/usr/data/config/` | empty `[ff_tool 0..3]` sections (the per-unit dock/nozzle data is autosaved, nothing unit-specific ships), `[ff_toolchange]` feeds/geometry, the `G28` dock-first wrapper | +| [`pkgs/klipper-config/payload/config/ff-tool-offset.cfg`](../pkgs/klipper-config/payload/config/ff-tool-offset.cfg) | `/usr/data/config/` | `[ff_tool_offset]` — probe geometry and guards for the calibration commands | +| [`pkgs/klipper-config/payload/config/ff-legacy.cfg`](../pkgs/klipper-config/payload/config/ff-legacy.cfg) | `/usr/data/config/` | `[ff_legacy]` — stays included permanently; declares the section and, optionally, `firmware_config_dir` | | [`pkgs/anvil-core/payload/bin/ffscreen.py`](../pkgs/anvil-core/payload/bin/ffscreen.py) | `/usr/data/anvil/bin/` | A few lines of text and a progress bar drawn straight onto `/dev/fb0`, geometry read from sysfs. The framebuffer is **portrait 480×800@32** and the panel is that buffer turned 90° clockwise (landscape 800×480) — established from FlashForge's own `/usr/prog/start.img`, 1536000 bytes, which only decodes into a picture read that way. Drawing is done in landscape coordinates and each rectangle is rotated on the way into the buffer, so it costs arithmetic per rectangle and nothing per pixel. All of the first boot happens before HelixScreen starts, so without it the panel is black for the longest wait of the install — which reads as a brick and invites a power cut mid-`SAVE_CONFIG`. `make boot-screen` renders every frame to PNG on the host; `make boot-screen-sim` renders the same frames inside the replica using FlashForge's own python3 on MIPS (they come out byte-identical); `qa/replica/test_boot_screen.py` is the gate | | [`pkgs/anvil-core/payload/bin/ff-startup.py`](../pkgs/anvil-core/payload/bin/ff-startup.py) | `/usr/data/anvil/bin/` | Everything before HelixScreen. **Every boot** it hands the toolhead boards over from their bootloaders (calling `ff_mcu_bringup.py` directly — it owns when klippy opens the ports, so it owns doing this first), starts klipper, and waits for klipper + moonraker to be ready, naming the board or service holding things up and re-handing the boards over on each retry. It runs as two s6-rc oneshots (`mcu-bringup`, then `ff-startup`), and the UI depends on the second — starting the UI before that is what produces a screen reporting a disconnected printer with no clue which board is missing. **First boot only**, once that has happened, it sends `FF_IMPORT_FIRMWARE_CONFIG` and `SAVE_CONFIG` over the moonraker API and stamps `/usr/data/anvil/.firmware-config-imported`. Only a verified save stamps, so a slow boot retries. `MOD_STARTUP=0` skips the lot; `MOD_IMPORT=0` keeps the wait and skips the migration | -| [`pkgs/anvil-core/payload/config/ff-print-macros.cfg`](../pkgs/anvil-core/payload/config/ff-print-macros.cfg) | `/usr/data/config/` | `START_PRINT` / `END_PRINT` / `PAUSE` / `RESUME` / `CANCEL_PRINT`, reconstructed from the app's sequences, plus the `_FF_PREFLIGHT` calibration and tool-presence gate; declares `[ff_print]` and the `FF_BEFORE_PRINT_START` / `FF_AFTER_PRINT_END` entry points it calls | -| [`pkgs/anvil-core/payload/config/ff-filament.cfg`](../pkgs/anvil-core/payload/config/ff-filament.cfg) | `/usr/data/config/` | `LOAD_FILAMENT` / `UNLOAD_FILAMENT` / `PURGE` — the touchscreen's filament-load sequence (grab tool, purge chute, feed) recovered from the binary; unload is a designed retract (the stock app has none) | -| [`pkgs/anvil-core/payload/config/ff-runout.cfg`](../pkgs/anvil-core/payload/config/ff-runout.cfg) | `/usr/data/config/` | Runout / clog handling: gives the stock `fd_ex*` / `fm_ex*` sensors a `runout_gcode` that pauses a Mainsail print when the **mounted** tool runs out or clogs (the app's E0162 / E0163, reported here in plain words); `ff_toolchange` arms only the mounted tool's sensors | +| [`pkgs/klipper-config/payload/config/ff-print-macros.cfg`](../pkgs/klipper-config/payload/config/ff-print-macros.cfg) | `/usr/data/config/` | `START_PRINT` / `END_PRINT` / `PAUSE` / `RESUME` / `CANCEL_PRINT`, reconstructed from the app's sequences, plus the `_FF_PREFLIGHT` calibration and tool-presence gate; declares `[ff_print]` and the `FF_BEFORE_PRINT_START` / `FF_AFTER_PRINT_END` entry points it calls | +| [`pkgs/klipper-config/payload/config/ff-filament.cfg`](../pkgs/klipper-config/payload/config/ff-filament.cfg) | `/usr/data/config/` | `LOAD_FILAMENT` / `UNLOAD_FILAMENT` / `PURGE` — the touchscreen's filament-load sequence (grab tool, purge chute, feed) recovered from the binary; unload is a designed retract (the stock app has none) | +| [`pkgs/klipper-config/payload/config/ff-runout.cfg`](../pkgs/klipper-config/payload/config/ff-runout.cfg) | `/usr/data/config/` | Runout / clog handling: gives the stock `fd_ex*` / `fm_ex*` sensors a `runout_gcode` that pauses a Mainsail print when the **mounted** tool runs out or clogs (the app's E0162 / E0163, reported here in plain words); `ff_toolchange` arms only the mounted tool's sensors | | [`pkgs/klipper-config/payload/config/printer.base.cfg`](../pkgs/klipper-config/payload/config/printer.base.cfg) | `$MODDIR/config/` -> symlinked to `/usr/data/config/printer.base.cfg` | FlashForge's `printer.base.cfg` with the chamber block replaced by `[include printer.chamber.cfg]`. Klipper can override an option but cannot un-declare a section, and the plain Creator 5 has no chamber heating element, so its heater must be **absent** rather than neutralised. `bin/unpack.sh` compares this against each stock package it unpacks and warns if FlashForge's has changed | | [`Creator5.cfg`](../pkgs/klipper-config/payload/config/chamber/Creator5.cfg) · [`Creator5Pro.cfg`](../pkgs/klipper-config/payload/config/chamber/Creator5Pro.cfg) | `$MODDIR/config/chamber/.cfg` -> symlinked to `/usr/data/config/printer.chamber.cfg` | The one per-model difference: the Pro gets `[heater_generic chamber_heater]` + `[verify_heater]` verbatim from FlashForge, the Creator 5 gets only `[temperature_sensor chamber]` on the same pin. **Both ship in `anvil-klipper-config`** and `anvil-link-prog.sh` links whichever the printer asks for, reading `MACHINE=` out of FlashForge's own `app_startup.sh`. They used to be a package per model, which Conflicted — each owned `config/printer.chamber.cfg`, so opkg refused the pair and the build had to choose. Nothing is edited at build time, and the payload is no longer model-specific | -| [`pkgs/anvil-core/payload/config/ff-chamber.cfg`](../pkgs/anvil-core/payload/config/ff-chamber.cfg) | `/usr/data/config/` | `M141` / `M191` for the chamber heater (Klipper has neither, and the stock app drove the chamber only from its own UI), plus the gate: the macros ask Klipper whether `heater_generic chamber_heater` exists, so a non-zero chamber target is refused on a machine that does not declare one. Nothing to keep in sync; identical in every package | +| [`pkgs/klipper-config/payload/config/ff-chamber.cfg`](../pkgs/klipper-config/payload/config/ff-chamber.cfg) | `/usr/data/config/` | `M141` / `M191` for the chamber heater (Klipper has neither, and the stock app drove the chamber only from its own UI), plus the gate: the macros ask Klipper whether `heater_generic chamber_heater` exists, so a non-zero chamber target is refused on a machine that does not declare one. Nothing to keep in sync; identical in every package | | [`docs/notes/`](notes/) | (reference only) | Condensed reverse-engineering notes: what the stock app actually does, with binary addresses | ## ⚠️ Before you start @@ -62,16 +62,17 @@ documented in the file headers. * **Still never edit the JSON under `/usr/data/firmwareRes/config/`.** This module no longer reads it, but the touchscreen app still does and rewrites those files wholesale — leave it alone. -* **Klipper lives on the firmware partition.** A FlashForge OTA update will - overwrite `/usr/prog/klipper/`, deleting the extras. Keep this repo and - re-run step 1 after every firmware update. The `#*#` block in - `printer.cfg` is on the data partition and survives. +* **Klipper lives on the data partition.** The extras install under + `/usr/data/anvil/klipper/klippy/extras/`, which a FlashForge OTA cannot + delete — but an OTA rewrites the software component the mod boots from, so + re-flash the mod after one. The `#*#` block in `printer.cfg` survives either + way. ## Install A package built from this repo needs no config editing at all. It ships: -* the `ff_*.py` extras, into the Klipper tree it flashes (`bin/patch.sh`) +* the `ff_*.py` extras, inside the klippy tree `anvil-klipper` installs * the `ff-*.cfg`, to `/usr/data/config/` (`run-append.sh`, keeping any you edited and leaving the new one as `.mod-new`) * the `[include]` lines for all seven, at the end of `printer.base.cfg` — @@ -93,12 +94,12 @@ root/ssh access is covered in the community [Discord](https://discord.gg/tYs3eNEDq)): ```sh -# 1. the klippy extras (firmware partition — may need remount rw) +# 1. the klippy extras (data partition — the tree the klipper service runs) scp pkgs/klipper/payload/klipper/klippy/extras/ff_*.py \ - pwned@PRINTER:/usr/prog/klipper/klippy/extras/ + pwned@PRINTER:/usr/data/anvil/klipper/klippy/extras/ # 2. the config files (data partition — survives OTA) -scp pkgs/anvil-core/payload/config/ff-*.cfg \ +scp pkgs/klipper-config/payload/config/ff-*.cfg \ pwned@PRINTER:/usr/data/config/ ``` @@ -371,7 +372,7 @@ dialog. Drop `pkgs/helixscreen/payload/helixscreen/config/printer_database.d/fla `config/printer_database.d/` for auto-detection (`ams_type: tool_changer`, `z_offset_calibration_strategy: firmware_managed`). Its default Load/Unload buttons only mount/unmount the tool (`SELECT_TOOL` / `UNSELECT_TOOL`); to actually feed or pull filament assign `LOAD_FILAMENT`, -`UNLOAD_FILAMENT` and `PURGE` from [`pkgs/anvil-core/payload/config/ff-filament.cfg`](../pkgs/anvil-core/payload/config/ff-filament.cfg) +`UNLOAD_FILAMENT` and `PURGE` from [`pkgs/klipper-config/payload/config/ff-filament.cfg`](../pkgs/klipper-config/payload/config/ff-filament.cfg) in Settings → Macro Buttons — a user-assigned macro outranks the backend, and the parameter dialog picks up `TOOL` / `TEMP` / `PURGE_TEMP` from the macros. diff --git a/docs/upgrading.md b/docs/upgrading.md index 5552220..e6000f2 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -60,9 +60,10 @@ your copy, landing the new version beside it as `.mod-new`. ## A FlashForge OTA update is a different thing - overwrite `/usr/prog/klipper/`, deleting the extras. Keep this repo and - re-run step 1 after every firmware update. The `#*#` block in - `printer.cfg` is on the data partition and survives. +Everything the mod installs is under `/usr/data/anvil` on the data partition, +the klippy tree and its extras included, so an OTA cannot delete them. What it +does rewrite is the software component the mod boots from — so re-flash the mod +after a firmware update. The `#*#` block in `printer.cfg` survives either way. --- diff --git a/installer/run-append.sh b/installer/run-append.sh index 9200a3c..666e0ca 100644 --- a/installer/run-append.sh +++ b/installer/run-append.sh @@ -263,10 +263,10 @@ sync # Overriding from printer.cfg AFTER the include survives every flash, while an # edit here is reverted by the next one. Each file says so in its header. # -# printer.base.cfg is on the same footing and needs no rule here: it installs -# to /usr/prog/klipper/config with the software component, which a flash -# replaces wholesale. printer.chamber.cfg used to be beside it and is not any -# more -- see the case below. +# printer.base.cfg is on the same footing and needs no rule here: it is +# anvil-klipper-config's, and anvil-link-prog.sh symlinks $MODDIR/config into +# /usr/data/config, so an upgrade repoints the link rather than editing a file. +# printer.chamber.cfg goes the same way -- see the case below. # # moonraker.conf is KEPT when edited. There is no include-and-override seam for # it, and a printer reached through a tuned trusted_clients or cors_domains diff --git a/pkgs/anvil-core/payload/anvil-env.sh b/pkgs/anvil-core/payload/anvil-env.sh index 4c56d37..ee450df 100644 --- a/pkgs/anvil-core/payload/anvil-env.sh +++ b/pkgs/anvil-core/payload/anvil-env.sh @@ -52,8 +52,8 @@ # # NOT libsodium either: the only importer of libnacl is Moonraker, which runs # on our 3.13, whose libnacl resolves $MODDIR/lib/libsodium.so by absolute path -# with no library path entry at all. klippy runs on 3.8 and has no libnacl -# import. +# with no library path entry at all. klippy is on that same 3.13 and has no +# libnacl import. ANVIL_LIBS="/usr/prog/Python-3.8.2/lib /usr/prog/openssl-1.0.2d/lib /usr/prog/libffi-3.4.4/lib" diff --git a/pkgs/anvil-core/payload/bin/anvil-link-prog.sh b/pkgs/anvil-core/payload/bin/anvil-link-prog.sh index 10ba379..e495488 100755 --- a/pkgs/anvil-core/payload/bin/anvil-link-prog.sh +++ b/pkgs/anvil-core/payload/bin/anvil-link-prog.sh @@ -10,12 +10,15 @@ # the only place anything is installed and `opkg upgrade` is enough to change # what the printer runs. # -# ORDERING. The software component carries firmwareExe and start.sh and stock -# run.sh copies them into place at its lines 107-179, before run-append.sh -# extracts the payload at line 180. So the component cannot ship the symlinks -# itself: on a first install they would dangle, and on an upgrade they would -# resolve to the payload being replaced. This runs after extraction, and again -# from anvil-core's postinst. +# ORDERING. Stock run.sh distributes the software component at its lines +# 107-179, before run-append.sh extracts the payload at line 180. So the +# component cannot ship these symlinks itself: on a first install they would +# dangle, and on an upgrade they would resolve to the payload being replaced. +# This runs after extraction, and again from anvil-core's postinst. The +# component no longer carries firmwareExe or start.sh at all -- nothing execs +# either before this point, and a wrapper without a payload behind it is worse +# than no wrapper (app_startup.sh restores the stock binary from a version +# directory; the wrapper would just sit there and never start a UI). set -e MODDIR=${MODDIR:-/usr/data/anvil} @@ -30,8 +33,9 @@ if [ ! -f /usr/prog/app_startup.sh ]; then fi # $1 is relative to $MODDIR, $2 is the absolute path the printer reads. The -# two destinations under /usr/prog mirror what stock run.sh does with the same -# files, so this stays true as long as that does: +# two destinations under /usr/prog are the ones stock run.sh copies to, so this +# stays true as long as that does -- its two cp lines are no-ops now that the +# component ships neither file: # cp $WORK_DIR/firmwareExe /usr/prog/PROGRAM/software/ # cp $WORK_DIR/start.sh /usr/prog/klipper/start.sh link_one() { diff --git a/pkgs/klipper-config/payload/config/ff-legacy.cfg b/pkgs/klipper-config/payload/config/ff-legacy.cfg index 07d80a0..a965ad0 100644 --- a/pkgs/klipper-config/payload/config/ff-legacy.cfg +++ b/pkgs/klipper-config/payload/config/ff-legacy.cfg @@ -20,7 +20,7 @@ # ff-legacy.cfg -- migration from firmwareExe's JSON. # # Keep it included (payload/klipper/extras/ff_legacy.py must be in -# /usr/prog/klipper/klippy/extras/). It registers ONE command and does +# /usr/data/anvil/klipper/klippy/extras/). It registers ONE command and does # nothing at startup: # # FF_IMPORT_FIRMWARE_CONFIG ; reads /usr/data/firmwareRes/config diff --git a/pkgs/klipper-config/payload/config/ff-tool-offset.cfg b/pkgs/klipper-config/payload/config/ff-tool-offset.cfg index d4bed7e..f6b11b3 100644 --- a/pkgs/klipper-config/payload/config/ff-tool-offset.cfg +++ b/pkgs/klipper-config/payload/config/ff-tool-offset.cfg @@ -20,7 +20,7 @@ # ff-tool-offset.cfg -- nozzle XY/Z offset calibration, Klipper-native. # # Implementation: payload/klipper/extras/ff_tool_offset.py (copy next to -# ff_toolchange.py in /usr/prog/klipper/klippy/extras/). +# ff_toolchange.py in /usr/data/anvil/klipper/klippy/extras/). # Recovered sequence: docs/notes/46-offset-calibration-recovered.md # # What it does diff --git a/pkgs/klipper-config/payload/config/ff-toolchange.cfg b/pkgs/klipper-config/payload/config/ff-toolchange.cfg index 673ef55..e1eea3e 100644 --- a/pkgs/klipper-config/payload/config/ff-toolchange.cfg +++ b/pkgs/klipper-config/payload/config/ff-toolchange.cfg @@ -111,16 +111,16 @@ # # --------------------------------------------------------------------------- # INSTALL -# A package built from this repo needs none of this: bin/patch.sh puts the -# ff_*.py extras into the Klipper tree it flashes, the stock run.sh copies +# A package built from this repo needs none of this: the ff_*.py extras +# ride inside the klippy tree anvil-klipper installs, the stock run.sh copies # the ff-*.cfg to /usr/data/config/, and printer.base.cfg ships the # [include] lines for all of them. Flashing is the whole install. # # By hand, onto a printer that is already modded: -# 1. cp payload/klipper/extras/ff_*.py /usr/prog/klipper/klippy/extras/ -# (Klipper lives on the firmware partition -- klipper_pri.sh runs -# /usr/prog/klipper/klippy/klippy.py. It may be mounted read-only, and an -# OTA will overwrite it, so keep a copy and a re-apply script.) +# 1. cp payload/klipper/extras/ff_*.py /usr/data/anvil/klipper/klippy/extras/ +# (that is the tree the klipper service runs, on the data partition, so an +# OTA cannot delete it -- but an OTA does rewrite the software component +# the mod boots from, so re-flash the mod after one.) # 2. cp the ff-*.cfg to /usr/data/config/ and make sure something includes # them -- printer.base.cfg does, at its end. All of them: the set is # mandatory, not a menu of optional drop-ins. Without it Klipper comes up diff --git a/pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py b/pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py index 713eaec..860c278 100644 --- a/pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py +++ b/pkgs/klipper/payload/klipper/klippy/extras/ff_toolchange.py @@ -16,7 +16,7 @@ # Driving the same MOTOR_* macros therefore inherits that behaviour unchanged. # # Install: copy ff_tool.py and ff_toolchange.py to -# /usr/prog/klipper/klippy/extras/, then see config/ff-toolchange.cfg. +# /usr/data/anvil/klipper/klippy/extras/, then see config/ff-toolchange.cfg. # # Klipper calls Tn only for lines that reach the gcode engine. We ship upstream # virtual_sdcard, which passes tool lines straight through, so a bare "Tn" from diff --git a/qa/replica/test_install.py b/qa/replica/test_install.py index fb9135f..59b2f09 100644 --- a/qa/replica/test_install.py +++ b/qa/replica/test_install.py @@ -55,6 +55,8 @@ pytestmark = pytest.mark.replica MODDIR = "/usr/data/anvil" FE = "/usr/prog/PROGRAM/software/firmwareExe" +# The other stock path anvil-link-prog.sh owns. klipperDaemon starts from here. +START = "/usr/prog/klipper/start.sh" APP = "/usr/prog/app_startup.sh" SOURCE = MODDIR + "/etc/s6-rc/source" DB = MODDIR + "/etc/s6-rc/compiled/current" @@ -141,6 +143,30 @@ def test_the_wrapper_starts_the_supervisor(box): "would start") +def test_the_stock_paths_are_symlinks_into_the_payload(box): + """The property the whole seam rests on since the software component + stopped carrying firmwareExe and start.sh. + + anvil-core installs both at $MODDIR/prog/ and anvil-link-prog.sh points the + stock paths at them -- from run-append.sh on a flash and from the postinst + on `opkg upgrade anvil-core`. A REGULAR FILE at either path means the link + step did not run, and the printer would go on executing whatever the last + install happened to leave there while an upgrade quietly rewrote $MODDIR. + """ + for path in (FE, START): + target = box.sh("readlink %s 2>/dev/null" % path).out.strip() + assert target, ( + "%s is not a symlink -- anvil-link-prog.sh did not run, so an " + "`opkg upgrade anvil-core` would not change what this printer " + "executes" % path) + assert target.startswith(MODDIR + "/prog/"), ( + "%s -> %r, which is not under %s/prog -- anvil-core is not what " + "this printer runs" % (path, target, MODDIR)) + assert box.file(path).exists, ( + "%s -> %r dangles: the link is there but the payload is not" + % (path, target)) + + def test_every_installed_script_parses_under_the_printers_busybox(box): """Every shell script the mod put on the machine, with the shell that will actually run it. A bashism here is a service that dies at boot.""" @@ -389,15 +415,20 @@ def test_a_boot_with_no_stick_does_not_try_to_update(box): firmwareExe is neutered for the run, because the real one holds the foreground forever by design. What is under test is the update block's decision, not the boot that follows it. + + The stub goes in by MOVING THE SYMLINK ASIDE, not by writing to $FE: that + path is a link into $MODDIR/prog now, so a redirection would write through + it and leave the packaged wrapper stubbed for whatever runs next. `mv` + moves the link itself, and moving it back restores it exactly. """ - box.sh("cp %s /tmp/fe.real && printf '#!/bin/sh\\nexit 0\\n' > %s" - % (FE, FE)) + box.sh("mv -f %s /tmp/fe.link && printf '#!/bin/sh\\nexit 0\\n' > %s " + "&& chmod +x %s" % (FE, FE, FE)) try: box.sh("sh %s > /tmp/boot-nostick.log 2>&1; echo rc=$?" % APP, timeout=300) log = box.file("/tmp/boot-nostick.log").text finally: - box.sh("cp /tmp/fe.real %s && chmod +x %s" % (FE, FE)) + box.sh("rm -f %s && mv -f /tmp/fe.link %s" % (FE, FE)) assert log.strip(), "app_startup.sh produced no output at all" assert "find update file" not in log, ( diff --git a/tools/python-packages/README.md b/tools/python-packages/README.md index 42de531..0850d9a 100644 --- a/tools/python-packages/README.md +++ b/tools/python-packages/README.md @@ -225,10 +225,10 @@ libsodium is ours -- and packaging and integration followed: the replica. 4. ~~Then the switch itself.~~ **Done**: `FF_PYTHON` in `anvil-env.sh` and `/usr/prog/libsodium/lib` off `ANVIL_LIBS`, in the same commit. -5. klippy is **not** blocked by packages, and was never part of this switch: - it keeps running on FlashForge's 3.8.2, started independently by - `/usr/prog/klipper/start.sh` (see `init.d/S70klipper`), not by - `FF_PYTHON`. cffi, greenlet, jinja2, markupsafe +5. klippy is **not** blocked by packages, and was not part of this switch -- + though it has since made the same move: the `klipper` s6-rc service execs + `$FF_PYTHON` against `$MODDIR/klipper/klippy`, so it is on 3.13 as + well. cffi, greenlet, jinja2, markupsafe and pyserial all build, and `c_helper.so` binds. `numpy` is the only gap, wanted solely by `extras/stepper_resonance_tester.py`, which Klipper loads on demand. numpy on 3.13 means numpy >= 2.1 and a Meson cross-file, and diff --git a/tools/python/README.md b/tools/python/README.md index 3065947..856caa7 100644 --- a/tools/python/README.md +++ b/tools/python/README.md @@ -252,6 +252,10 @@ FlashForge's binary. ## It ships. Nothing runs it. +*[Superseded: `FF_PYTHON` is `$MODDIR/bin/python3.13` now, and Moonraker, +klippy, `ff-startup.py`, `ffscreen.py` and the MCU bring-up all run on it. What +follows is the state this investigation ended in.]* + `payload/anvil-env.sh` still points `FF_PYTHON` at `/usr/prog/Python-3.8.2/bin/python3`. That is timing, not preference: klippy, Moonraker and `ff-startup.py` import third-party C extensions -- tornado, diff --git a/versions.env b/versions.env index 6a82e09..ecdf41a 100644 --- a/versions.env +++ b/versions.env @@ -156,9 +156,9 @@ S6RC_SHA256="bf5b8ce0da5a4ee70d642b818b61d9916a7a9b64a457595f388113e54a188688" # toolchain like everything else, and this tree has one libc again. # ---------------------------------------------------------------- CPython -# A SECOND Python for the printer, cross-compiled here and SHIPPED BUT NOT -# USED (bin/patch.sh section 5c). FlashForge's own /usr/prog/Python-3.8.2 is -# still what everything runs on -- see payload/anvil-env.sh. +# A SECOND Python for the printer, cross-compiled here and SHIPPED AND USED: +# $FF_PYTHON names it, so Moonraker, klippy, ff-startup.py, ffscreen.py and the +# MCU bring-up all run on it -- see payload/anvil-env.sh. # # WHY IT EXISTS AT ALL. FlashForge built 3.8.2 without _sqlite3 and shipped no # libsqlite3 anywhere on the rootfs, which is the single fact that pins @@ -168,7 +168,12 @@ S6RC_SHA256="bf5b8ce0da5a4ee70d642b818b61d9916a7a9b64a457595f388113e54a188688" # rather than a 3.8 with one module added because a from-source build costs # the same either way. Measured on the replica: the stock 3.8.2 cannot import # sqlite3 and this one creates a database, writes it, closes it and reads it -# back. test/integration/printer/case-python.sh is that measurement, kept. +# back. That standalone measurement was retired with the case scripts; what is +# run on every replica now is qa/replica/test_web.py, which brings real +# Moonraker up on this interpreter and waits for s6's `ready`. It exercises the +# interpreter harder and says nothing about sqlite3 directly -- the pinned +# Moonraker predates the sqlite database, which is the whole reason it is +# pinned. # # EVERY VERSION BELOW IS PINNED WITH A sha256 FOR THE SAME REASON THE REST OF # THIS FILE IS: nothing binary is vendored, so the only thing standing between @@ -241,10 +246,10 @@ EXPAT_SHA256="fd03b7172b3bd7427a3e7a812063f74754f24542429b634e0db6511b53fb2278" # ------------------------------------------- CPython's third-party packages # The 18 sdists that become $MODDIR/lib/python3.13/site-packages -- what -# Moonraker and klippy import, cross-built for mipsel by bin/patch.sh section -# 5c against the interpreter pinned above. SHIPPED BUT NOT USED, exactly like -# the interpreter itself: payload/anvil-env.sh still points FF_PYTHON at -# FlashForge's 3.8.2 and the switch is a later commit. +# Moonraker and klippy import, cross-built for mipsel against the interpreter +# pinned above -- one pkgs/3rdparty/python-* recipe and one .ipk each. They are +# live, exactly like the interpreter itself: payload/anvil-env.sh points +# FF_PYTHON at it and klippy is among its callers. # # THE LIST IS NOT moonraker-requirements.txt, and that is a decision rather # than an omission. That file installs every OPTIONAL component's