Fix the runout insert path and TOOL=ALL staging; drop SAFE-MODE for MOD_UI

A2: _FF_INSERT's body was entirely inside {% if kind == 'switch' %}, yet
insert_gcode is wired on all four fm_ex* with KIND=motion. A motion insert --
which is what clearing a clog looks like, filament moving again -- produced no
output at all, on exactly the path where the operator most needs the next
step. Adds the motion branch the comment already described.

A3: TOOL_OFFSET_CALIBRATE staged each tool with set_nozzle() inside the loop,
so a gap-check failure on a later tool left earlier tools pending for
SAVE_CONFIG -- while raising an error that says "nothing saved". True for
TOOL=<n>, false for TOOL=ALL, and told to the operator at the moment they
decide whether to save. Staging now happens after every tool has passed.

A8: SAFE-MODE is removed rather than repaired. It never worked: the settle
check cleared the counter 90s after every boot on a condition
(`[ -s $CHOICE ]`) that cannot be false, since $CHOICE is written two lines
earlier and "none" is not empty. Nothing checked whether helix-screen was
alive, so the latch only fired on three reboots inside 90 seconds -- not the
crash-loop it was written for.

In its place, MOD_UI in anvil.conf, alongside MOD_WEB/MOD_CAM/MOD_SSH/MOD_WIFI
and templated by patch.sh the same way. It stops the UI being started; it does
not pretend to repair one. The docs said `touch SAFE-MODE` was how you recover
a blank screen. Nothing recovers a blank screen on the machine: HelixScreen is
the only UI, and getting one back means flashing a package. Said plainly now,
in the README, the recovery section and the troubleshooting table -- along with
what is genuinely unaffected, which is ssh, Mainsail, Klipper and printing.

48 passed, 5 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oleksandr ShyshatskyiandClaude Opus 5 committed 2026-08-24 19:48:42 +03:00
1 parent e5bd171cf8
commit b2c651b11f
11 files changed
+62 -58

No files matched your search

+7 -4
View File
@@ -102,10 +102,13 @@ installs the same way — stick in, power on.
A few things are deliberately built in so a bad flash cannot cost you the
machine:
* **A crashing UI cannot lock you out.** If HelixScreen cannot start or
crashes repeatedly, the printer latches SAFE-MODE and boots *headless* —
no UI at all — instead of looping. Delete `/usr/data/anvil/SAFE-MODE` over
ssh to try again.
* **A crashing UI cannot lock you out.** The screen is not how you reach the
printer: ssh and Mainsail come up from their own services, before and
independently of the UI, so a dead screen costs you the screen and nothing
else — you can still print. It is not repairable on the machine, though:
HelixScreen is the only UI there is, and getting a working screen back means
flashing a package again. `MOD_UI=0` in `/usr/data/anvil/anvil.conf` stops
it being started at all.
* **ssh stays up** even when the screen does not, so you can get in and look.
* **Your `printer.cfg` is never overwritten** — it is not a file this
firmware ships, and it is where your changes belong. The configs the mod
+5 -4
View File
@@ -27,10 +27,10 @@ fi
# above -- still overrides any of them.
#
# FlashForge's firmwareExe is REPLACED, not kept: HelixScreen is the only UI.
# If it crash-loops, S80ui latches SAFE-MODE after 3 failed boots and the
# printer boots headless instead of looping. ssh and Mainsail are your
# recovery path if the screen is dark, and a USB stick with the STOCK
# FlashForge package on it is the uninstall (proven by `make test-recovery`).
# If it will not start, set MOD_UI=0 in /usr/data/anvil/anvil.conf to boot
# headless. ssh and Mainsail are your recovery path if the screen is dark, and
# a USB stick with the STOCK FlashForge package on it is the uninstall (proven
# by `make test-recovery`).
BUILD_KLIPPER="${BUILD_KLIPPER:-fork}"
BUILD_TOOLCHANGE="${BUILD_TOOLCHANGE:-1}"
BUILD_MAINSAIL="${BUILD_MAINSAIL:-1}"
@@ -38,6 +38,7 @@ BUILD_MOONRAKER="${BUILD_MOONRAKER:-1}"
BUILD_HELIX="${BUILD_HELIX:-1}"
MOD_SSH="${MOD_SSH:-1}"
MOD_WEB="${MOD_WEB:-1}"
MOD_UI="${MOD_UI:-1}"
# Third-party payload pieces (Mainsail, HelixScreen, Moonraker). They are
# downloaded on demand rather than vendored, so the repo carries no binaries
+1
View File
@@ -288,6 +288,7 @@ cp -f payload/init.d/S* "$MP/init.d/"
chmod +x "$MP/init.d"/S*
sed -e "s/^MOD_WEB=.*/MOD_WEB=${MOD_WEB:-1}/" \
-e "s/^MOD_CAM=.*/MOD_CAM=${MOD_CAM:-1}/" \
-e "s/^MOD_UI=.*/MOD_UI=${MOD_UI:-1}/" \
-e "s/^MOD_SSH=.*/MOD_SSH=${MOD_SSH:-1}/" \
-e "s/^MOD_WIFI=.*/MOD_WIFI=${MOD_WIFI:-1}/" \
payload/anvil.conf > "$MP/anvil.conf"
+17 -8
View File
@@ -102,9 +102,10 @@ screen and there is nothing to fall back to:
`firmwareExe` — not any init script — that ran `/usr/prog/klipper/start.sh`.
Without this the printer would boot to a working screen and be unable to
move.
- If the UI dies repeatedly, `SAFE-MODE` latches after 3 boots and the printer
boots **headless** — no UI started at all — rather than crash-looping. ssh
and Mainsail stay up. Delete `/usr/data/anvil/SAFE-MODE` to try again.
- If the UI misbehaves, set `MOD_UI=0` in `/usr/data/anvil/anvil.conf` and
reboot: the printer comes up **headless** — no UI started at all. ssh and
Mainsail are unaffected either way. There is no automatic latch; nothing
stops the UI unless you do.
**Go/no-go, in this order — with the emergency stop within reach:**
@@ -223,14 +224,22 @@ every flash. Each of those files opens with a header saying as much.
`moonraker.conf` is the one exception — it has no include-and-override seam, so
an edited copy is kept and the new version lands beside it as `.mod-new`.
**Recovering from a bad UI, in increasing severity:**
**A bad UI is not repairable on the printer.** There is no fallback interface:
FlashForge's binary is replaced and HelixScreen is the only thing that draws on
the screen. ssh tells you *why* it is dark, but nothing you can set on the
machine will make it light up again:
```sh
ssh root@PRINTER
/usr/data/anvil/init.d/S80ui status # what did it choose, and why
touch /usr/data/anvil/SAFE-MODE # boot headless next time (ssh + Mainsail only)
reboot
# chose "none" -> MOD_UI=0, or HelixScreen is not installed
# chose "helix" -> it was started and failed on its own; see its log
```
If ssh is gone too, flash the stock FlashForge package for your model.
`MOD_UI=0` only stops it being started at all — useful to silence a UI that
interferes with something else, not a repair. The actual fix is to flash a
package again: the mod, if you have a corrected build, or the stock FlashForge
package for your model to go back to FlashForge's own interface.
---
@@ -238,7 +247,7 @@ If ssh is gone too, flash the stock FlashForge package for your model.
| Symptom | Do this |
|---|---|
| Printer boots, screen blank | ssh in; `/usr/data/anvil/init.d/S80ui status`; `touch /usr/data/anvil/SAFE-MODE`; reboot |
| Printer boots, screen blank | ssh in; `/usr/data/anvil/init.d/S80ui status` says whether the UI was even started. No on-device repair — reflash the mod, or flash the stock package to get FlashForge's UI back. ssh, Mainsail and printing are unaffected |
| No ssh, no screen | flash the stock package for your model |
| Recovery stick does not help | try a newer stock FlashForge package for your model |
| Still broken | factory package (`Creator5Pro-factory-*.tgz` **plus** the separate `Creator5Pro-factory.tar.xz` on the same stick; needs 800 MB free) |
+1 -1
View File
@@ -43,7 +43,7 @@ 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
S80ui decides whether the UI runs (MOD_UI, HelixScreen installed?)
```
Everything the mod installs lives under `/usr/data/anvil` on the **data**
+3 -3
View File
@@ -234,9 +234,9 @@ Tests that cannot fail are worse than no tests, because they read as coverage:
to it. Its allowlist had drifted to nine entries that excused nothing. The
absolute-path half survives, because `S50wifi`'s binaries sit on a branch
no simulation reaches.
- `lint-danger`'s structural greps were satisfiable by comments —
`firmwareExe:44` and `S80ui:16` *mention* `helix` and `SAFE-MODE`, so the
checks passed with the logic deleted.
- `lint-danger`'s structural greps were satisfiable by comments — the
comments in `firmwareExe` and `S80ui` *mention* `helix` and (at the time)
`SAFE-MODE`, so the checks passed with the logic deleted.
- `case-ui` went with the helix-only decision.
## Bugs these caught
+4
View File
@@ -14,6 +14,10 @@ MOD_WEB=1
#MOD_CAM_PORT=8080
MOD_CAM=1
# Touchscreen UI: HelixScreen, started by the firmwareExe wrapper.
# 0 = boot headless (blank screen); ssh and Mainsail are unaffected.
MOD_UI=1
# SSH: dropbear on :22
MOD_SSH=1
+2 -2
View File
@@ -40,8 +40,8 @@ if [ -d $MODDIR/init.d ]; then
fi
# ---- the UI, in the foreground so the stock watchdog can see us ----------
# S80ui already handled SAFE-MODE and crash counting and wrote its verdict
# here: "helix" to start the UI, "none" to stay headless.
# S80ui already decided and wrote its verdict here: "helix" to start the UI,
# "none" to stay headless (MOD_UI=0, or HelixScreen not installed).
UI_CHOICE=`cat $MODDIR/.ui-choice 2>/dev/null || echo none`
echo "ui: choice is $UI_CHOICE"
+9 -33
View File
@@ -8,19 +8,14 @@
# foreground, so it only chooses.
#
# HelixScreen is the only UI: FlashForge's firmwareExe is retired and there is
# nothing to fall back to. So the safety rules encoded here are what stands
# between a crash-looping UI and losing the machine:
# * ssh comes from the stock /etc/init.d/S50dropbear, which busybox init
# already ran long before this script. It is up regardless of what
# happens here, and so are S60web (Mainsail) and S70klipper.
# * A UI that crash-loops latches SAFE-MODE after MAX_UI_FAILURES boots and
# the printer then boots HEADLESS -- no UI at all -- rather than looping.
# * Delete /usr/data/anvil/SAFE-MODE (over ssh) to try the UI again.
# nothing to fall back to. There is no crash counter and no automatic headless
# latch -- the UI is started whenever it is installed and MOD_UI is not 0.
# To stop it, set MOD_UI=0 in /usr/data/anvil/anvil.conf and reboot; ssh comes
# from the stock /etc/init.d/S50dropbear, which busybox init ran long before
# this script, so it is up regardless of what happens here, and so are S60web
# (Mainsail) and S70klipper.
MODDIR=/usr/data/anvil
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
MAX_UI_FAILURES="${MOD_MAX_UI_FAILURES:-3}"
FAILFILE=$MODDIR/ui-failures
SAFEFILE=$MODDIR/SAFE-MODE
CHOICE=$MODDIR/.ui-choice
have_helix() { [ -x $MODDIR/helixscreen/bin/helix-launcher.sh ] || [ -x $MODDIR/helixscreen/bin/helix-screen ]; }
@@ -28,20 +23,8 @@ have_helix() { [ -x $MODDIR/helixscreen/bin/helix-launcher.sh ] || [ -x $MODDIR/
start() {
want=helix
if [ -f "$SAFEFILE" ]; then
echo "ui: SAFE-MODE latched -- staying headless (delete $SAFEFILE to retry)"
want=none
fi
failures=`cat "$FAILFILE" 2>/dev/null || echo 0`
case "$failures" in ''|*[!0-9]*) failures=0 ;; esac
# Every boot increments the counter; the UI clears it once it has been
# alive for a while (see the settle check below). A crash-loop therefore
# walks the counter up until SAFE-MODE latches.
if [ "$failures" -ge "$MAX_UI_FAILURES" ]; then
echo "ui: $failures consecutive short boots -- latching SAFE-MODE"
: > "$SAFEFILE"
if [ "${MOD_UI:-1}" = 0 ]; then
echo "ui: MOD_UI=0 in anvil.conf -- staying headless"
want=none
fi
@@ -56,12 +39,6 @@ start() {
echo "$want" > "$CHOICE"
echo "ui: chose $want"
# Arm the failure counter, then clear it in the background once the UI has
# survived long enough to be considered healthy.
echo `expr $failures + 1` > "$FAILFILE"
( sleep 90
if [ -s "$CHOICE" ]; then rm -f "$FAILFILE"; fi ) &
}
stop() {
killall helix-screen helix-watchdog helix-splash 2>/dev/null
@@ -72,7 +49,6 @@ case "$1" in
stop) stop ;;
restart) stop; sleep 2; start ;;
status) echo "ui choice: `cat $CHOICE 2>/dev/null || echo unset`";
echo "failures: `cat $FAILFILE 2>/dev/null || echo 0`";
[ -f "$SAFEFILE" ] && echo "SAFE-MODE: latched" || echo "SAFE-MODE: clear" ;;
echo "MOD_UI: ${MOD_UI:-1}" ;;
*) echo "usage: $0 {start|stop|restart|status}"; exit 1 ;;
esac
+6 -1
View File
@@ -155,7 +155,8 @@ gcode:
# Called by the sensors' insert_gcode (Klipper only fires it while idle, so
# this never interrupts a moving print). The app logged "insert" and, for
# the motion sensors, cleared its clog flag; we just tell the user what to
# do next.
# do next. A switch insert means filament is present again; a motion insert
# means it is MOVING again, which is what clearing a clog looks like.
# ---------------------------------------------------------------------------
[gcode_macro _FF_INSERT]
gcode:
@@ -167,4 +168,8 @@ gcode:
{% elif not printer.pause_resume.is_paused %}
{ action_respond_info("Filament detected in T%d: LOAD_FILAMENT TOOL=%d to feed it." % (tool, tool)) }
{% endif %}
{% else %}
{% if printer.pause_resume.is_paused and tool == printer.ff_toolchange.current_tool %}
{ action_respond_info("T%d is moving filament again: RESUME to continue (or CANCEL_PRINT)." % tool) }
{% endif %}
{% endif %}
+7 -2
View File
@@ -545,12 +545,17 @@ class FFToolOffset:
self.last['t%d' % tool] = (cx, cy, zp)
gcmd.respond_info("T%d: offset = (%.4f, %.4f, %.4f)"
% (tool, cx, cy, zp))
if save:
self.tools[tool].set_nozzle(cx, cy, zp)
if release:
self._run('TOOLCHANGE_PARK')
self._wait_moves()
# Staged only once every tool has passed its guards, so that a failure
# part-way through TOOL=ALL really does leave nothing saved -- which is
# what the guard messages above promise.
if save:
for tool, (cx, cy, zp) in results.items():
self.tools[tool].set_nozzle(cx, cy, zp)
# the app's exit block: heater off for the tool(s), Z15
for tool in tools:
self._run('M104 S0 T%d' % tool)