From 4704ca0213b69fbd454cdb820128776dc53fb1ae Mon Sep 17 00:00:00 2001 From: Oleksandr Shyshatskyi Date: Tue, 25 Aug 2026 22:37:38 +0300 Subject: [PATCH] Names that say what they hold A sweep over the payload, the build scripts and the test harness, replacing contracted identifiers with ones that state their subject. No behaviour changes: the boot-screen frames render byte-identical before and after, and `make test` goes green through unpack, patch, pack and verify. The ones that were actively misleading: ff_tool_offset.py px1/py2/px3/py4 were the four probe points, but the digits are ordinal (probe order) while the letters are axes -- so `px3` is the -X point, not the third X. Now x_plus/y_plus/x_minus/y_minus. zp/zp2 -> z_trigger/z_trigger_pass2, za -> z_adjust, xbar/ybar -> centroid_x/centroid_y. ff_toolchange.py off_x/off_y/off_z read as "offset off", and they are two different quantities: X/Y are differences against the base tool, Z is absolute. offset_*. tc -> toolchanger, cur -> mounted, why -> reason. S60web MR_PY/MR_MAIN/MR_TMP -> MOONRAKER_*. MR_PY in particular reads as a file, not an interpreter. run-append.sh MRIMP -> MOONRAKER_PREFLIGHT_FAILED. It is a failure flag whose name said "import". bin/verify.sh T -> WORK, SW -> SOFTWARE_TAR, CH -> CHELPER. Left alone deliberately: fd, gcmd, cmd, rgb, and single-letter loop variables over an obvious list. They are the vocabulary of their domain, not contractions of it. One collision found and fixed on the way: test/run-tests.py already had a `fixture` (the stock package), so the moonraker one is moonraker_fixture and the stock one is stock_package. The first pass shadowed it and the fixture build failed -- which is the argument for the whole change. Co-Authored-By: Claude Opus 5 (1M context) --- bin/build.sh | 10 +- bin/patch.sh | 114 +++--- bin/preview-boot-screen.py | 52 +-- bin/verify.sh | 96 +++--- payload/bin/ff-startup.py | 209 +++++------ payload/bin/ff_mcu_bringup.py | 92 ++--- payload/bin/ffscreen.py | 76 ++-- payload/firmwareExe | 16 +- payload/init.d/S50wifi | 12 +- payload/init.d/S60web | 50 +-- payload/init.d/S65camera | 6 +- payload/init.d/S70klipper | 31 +- payload/klipper/extras/ff_legacy.py | 221 ++++++------ payload/klipper/extras/ff_print.py | 69 ++-- payload/klipper/extras/ff_tool.py | 12 +- payload/klipper/extras/ff_tool_offset.py | 381 ++++++++++---------- payload/klipper/extras/ff_toolchange.py | 383 +++++++++++---------- payload/moonraker-preflight.py | 30 +- payload/run-append.sh | 124 +++---- payload/run-pre.sh | 9 +- payload/start.sh | 6 +- test/ffcfg.py | 28 +- test/ffsim/config.py | 26 +- test/ffsim/gates.py | 12 +- test/ffsim/replica.py | 32 +- test/integration/printer/case-moonraker.sh | 8 +- test/integration/test_moonraker_libpath.py | 4 +- test/run-tests.py | 187 +++++----- 28 files changed, 1199 insertions(+), 1097 deletions(-) diff --git a/bin/build.sh b/bin/build.sh index a9964b4..4fbb622 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -5,8 +5,8 @@ # ./bin/build.sh --full options are passed through to pack.sh # (--slim is accepted but is the default) set -euo pipefail -D="$(dirname "$0")" -"$D/fetch-assets.sh" -"$D/unpack.sh" -"$D/patch.sh" -"$D/pack.sh" "$@" +SCRIPT_DIR="$(dirname "$0")" +"$SCRIPT_DIR/fetch-assets.sh" +"$SCRIPT_DIR/unpack.sh" +"$SCRIPT_DIR/patch.sh" +"$SCRIPT_DIR/pack.sh" "$@" diff --git a/bin/patch.sh b/bin/patch.sh index 0ec6cd0..a88823c 100755 --- a/bin/patch.sh +++ b/bin/patch.sh @@ -4,8 +4,8 @@ set -euo pipefail . "$(dirname "$0")/common.sh" -SW=work/software -[ -d "$SW" ] || { echo "run bin/unpack.sh first" >&2; exit 1; } +SOFTWARE_DIR=work/software +[ -d "$SOFTWARE_DIR" ] || { echo "run bin/unpack.sh first" >&2; exit 1; } say() { printf '>> %s\n' "$*"; } skip() { printf ' (skip) %s\n' "$*"; } @@ -19,9 +19,9 @@ MODDIR=/usr/data/anvil # HelixScreen are ~100MB and would overflow it. They ride in the outer package # instead, land in /usr/data/update/ (data partition), and are moved to # /usr/data/anvil from there. -MP=work/modpayload -rm -rf "$MP" "$SW/mod" # $SW/mod: leftover from an older layout -mkdir -p "$MP/bin" "$MP/nginx" "$MP/www" "$MP/config" +MOD_PAYLOAD=work/modpayload +rm -rf "$MOD_PAYLOAD" "$SOFTWARE_DIR/mod" # $SOFTWARE_DIR/mod: leftover from an older layout +mkdir -p "$MOD_PAYLOAD/bin" "$MOD_PAYLOAD/nginx" "$MOD_PAYLOAD/www" "$MOD_PAYLOAD/config" # ---------------------------------------------------------------- 1. Klipper # BUILD_KLIPPER=fork (the default) ships the creator5 Klipper tree; =stock @@ -63,9 +63,9 @@ fork) # exists is not trusted with the tree's freshness: one that outlived its # sources is how a v0.12-generation binary once shipped under a v0.13 # klippy (see test/test-chelper.py's docstring). - CH="$FORK/klippy/chelper/c_helper.so" - if [ ! -f "$CH" ] || [ -n "$(find "$FORK/klippy/chelper" \ - \( -name '*.c' -o -name '*.h' \) -newer "$CH" -print -quit)" ]; then + CHELPER="$FORK/klippy/chelper/c_helper.so" + if [ ! -f "$CHELPER" ] || [ -n "$(find "$FORK/klippy/chelper" \ + \( -name '*.c' -o -name '*.h' \) -newer "$CHELPER" -print -quit)" ]; then GCC="work/.mips-toolchain/mips-gcc720-glibc229/bin/mips-linux-gnu-gcc" if [ ! -x "$GCC" ]; then [ -f "${MIPS_TOOLCHAIN_TGZ:-}" ] || { @@ -81,13 +81,13 @@ fork) # printer would use if it could compile, which it cannot. "$GCC" -Wall -g -O2 -shared -fPIC \ -flto -fwhole-program -fno-use-linker-plugin \ - -o "$CH" "$FORK"/klippy/chelper/*.c + -o "$CHELPER" "$FORK"/klippy/chelper/*.c fi # The gates. ABI: the kernel refuses anything but MIPS32r2/nan2008/o32. # Symbols: cffi resolves lazily, so a stale .so dies at connect on the # printer instead of at import in the build -- catch it here. - if ! readelf -h "$CH" 2>/dev/null | grep -q nan2008; then + if ! readelf -h "$CHELPER" 2>/dev/null | grep -q nan2008; then echo " !! c_helper.so is NOT nan2008 -- the kernel will refuse it" >&2 exit 1 fi @@ -96,19 +96,19 @@ fork) # Stock ships only a handful of klippy files as an overlay; the fork is a # different Klipper generation (v0.13 vs v0.12), so ship the WHOLE tree. - rm -rf "$SW/klipper/klippy" - mkdir -p "$SW/klipper/klippy" + rm -rf "$SOFTWARE_DIR/klipper/klippy" + mkdir -p "$SOFTWARE_DIR/klipper/klippy" ( cd "$FORK/klippy" && tar -cf - \ --exclude='__pycache__' --exclude='*.pyc' --exclude='chelper/*.o' . ) \ - | tar -xf - -C "$SW/klipper/klippy" + | tar -xf - -C "$SOFTWARE_DIR/klipper/klippy" mkdir -p work/.chelper/chelper - cp -f "$CH" work/.chelper/chelper/c_helper.so - tar -cf "$SW/klipper/chelper.tar" -C work/.chelper chelper + cp -f "$CHELPER" work/.chelper/chelper/c_helper.so + tar -cf "$SOFTWARE_DIR/klipper/chelper.tar" -C work/.chelper chelper rm -rf work/.chelper # klippy/ now contains the fork's own extras+kinematics, so the stock # overlay dirs would only re-inject 0.12-era files on top. Drop them. - rm -rf "$SW/klipper/extras" "$SW/klipper/kinematics" - mkdir -p "$SW/klipper/extras" + rm -rf "$SOFTWARE_DIR/klipper/extras" "$SOFTWARE_DIR/klipper/kinematics" + mkdir -p "$SOFTWARE_DIR/klipper/extras" ;; stock) skip "Klipper: keeping stock tree (BUILD_KLIPPER=stock)" @@ -124,12 +124,12 @@ esac # creator5-toolchange checkout, pointed at by TOOLCHANGE= in config.env. if [ "${BUILD_TOOLCHANGE:-1}" = "1" ]; then say "Toolchange: ff_*.py + configs" - mkdir -p "$SW/klipper/extras" - cp -f payload/klipper/extras/ff_*.py "$SW/klipper/extras/" + mkdir -p "$SOFTWARE_DIR/klipper/extras" + cp -f payload/klipper/extras/ff_*.py "$SOFTWARE_DIR/klipper/extras/" # .cfg files belong on the data partition. These are mod-owned: run.sh # overwrites them on every update (test_config_ownership.py enforces it). # User changes go in printer.cfg, which no flash ever writes. - cp -f payload/klipper/config/ff-*.cfg "$MP/config/" + cp -f payload/klipper/config/ff-*.cfg "$MOD_PAYLOAD/config/" # Our printer.base.cfg is FlashForge's 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 @@ -137,7 +137,7 @@ if [ "${BUILD_TOOLCHANGE:-1}" = "1" ]; then # NOTE: this cp is why the stock-drift check lives in bin/unpack.sh and # not in a test -- it overwrites the pristine copy, and the test that used # to read it afterwards was comparing our file against itself. - cp -f payload/klipper/config/printer.base.cfg "$SW/klipper/config/printer.base.cfg" + cp -f payload/klipper/config/printer.base.cfg "$SOFTWARE_DIR/klipper/config/printer.base.cfg" # Anything that differs between models exists once per model, named # .creator5 / .creator5pro, and the matching one is installed @@ -149,15 +149,15 @@ if [ "${BUILD_TOOLCHANGE:-1}" = "1" ]; then [ -e "$variant" ] || continue base=$(basename "$variant" ".$SUFFIX") case "$base" in - printer.*) dest="$SW/klipper/config/$base" ;; - *) dest="$MP/config/$base" ;; + printer.*) dest="$SOFTWARE_DIR/klipper/config/$base" ;; + *) dest="$MOD_PAYLOAD/config/$base" ;; esac cp -f "$variant" "$dest" say "Model: $base for $TARGET_MACHINE" done # printer.base.cfg includes it unconditionally, so without it klippy will # not start at all. A broken build, not a silent default. - [ -f "$SW/klipper/config/printer.chamber.cfg" ] \ + [ -f "$SOFTWARE_DIR/klipper/config/printer.chamber.cfg" ] \ || { echo "no printer.chamber.cfg.$SUFFIX for TARGET_MACHINE=$TARGET_MACHINE" >&2; exit 1; } else skip "Toolchange" @@ -170,18 +170,18 @@ if [ "${BUILD_MAINSAIL:-1}" = "1" ]; then # should have put it here. [ -f "${MAINSAIL_ZIP:-}" ] || { echo "BUILD_MAINSAIL=1 but no Mainsail zip at '${MAINSAIL_ZIP:-}' -- run ./bin/fetch-assets.sh" >&2; exit 1; } say "Mainsail: unpacking $(basename "$MAINSAIL_ZIP")" - mkdir -p "$MP/www/mainsail" - unzip -q -o "$MAINSAIL_ZIP" -d "$MP/www/mainsail" - cp -f assets/nginx.conf "$MP/nginx/nginx.conf" - du -sh "$MP/www/mainsail" | awk '{print " "$1}' + mkdir -p "$MOD_PAYLOAD/www/mainsail" + unzip -q -o "$MAINSAIL_ZIP" -d "$MOD_PAYLOAD/www/mainsail" + cp -f assets/nginx.conf "$MOD_PAYLOAD/nginx/nginx.conf" + du -sh "$MOD_PAYLOAD/www/mainsail" | awk '{print " "$1}' else skip "Mainsail" fi -[ -f assets/moonraker.conf ] && cp -f assets/moonraker.conf "$MP/config/moonraker.conf" +[ -f assets/moonraker.conf ] && cp -f assets/moonraker.conf "$MOD_PAYLOAD/config/moonraker.conf" # The user seam for Moonraker. moonraker.conf includes it by name, and # run-append.sh creates it only when it is missing -- never overwrites it. [ -f assets/moonraker-custom.conf ] \ - && cp -f assets/moonraker-custom.conf "$MP/config/moonraker-custom.conf" + && cp -f assets/moonraker-custom.conf "$MOD_PAYLOAD/config/moonraker-custom.conf" # ------------------------------------------------------------- 4. Moonraker # WHY THIS EXISTS -- the stock Moonraker is a 2022 build (it reports API @@ -246,17 +246,17 @@ if [ "${BUILD_MOONRAKER:-1}" = "1" ]; then # shipping nothing here would look like a clean build and a dead UI. [ -f work/.moonraker/moonraker/moonraker.py ] || { echo " !! no moonraker/moonraker.py in $(basename "$MOONRAKER_TGZ")" >&2; exit 1; } - rm -rf "$MP/moonraker" - cp -a work/.moonraker/moonraker "$MP/moonraker" + rm -rf "$MOD_PAYLOAD/moonraker" + cp -a work/.moonraker/moonraker "$MOD_PAYLOAD/moonraker" # The gate run-append.sh puts in front of the swap. It reads Moonraker's # own component list out of the tree beside it, so it does not need # updating when the pin moves. - cp -f payload/moonraker-preflight.py "$MP/moonraker-preflight.py" + cp -f payload/moonraker-preflight.py "$MOD_PAYLOAD/moonraker-preflight.py" # Tests never run on the printer and are a sizeable chunk of the tree. - rm -rf "$MP/moonraker/tests" - find "$MP/moonraker" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null + rm -rf "$MOD_PAYLOAD/moonraker/tests" + find "$MOD_PAYLOAD/moonraker" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null rm -rf work/.moonraker - du -sh "$MP/moonraker" | awk '{print " "$1}' + du -sh "$MOD_PAYLOAD/moonraker" | awk '{print " "$1}' else skip "Moonraker: keeping the stock 2022 build" fi @@ -265,17 +265,17 @@ fi if [ "${BUILD_HELIX:-1}" = "1" ]; then [ -f "${HELIX_TGZ:-}" ] || { echo "BUILD_HELIX=1 but no HelixScreen tarball at '${HELIX_TGZ:-}' -- run ./bin/fetch-assets.sh" >&2; exit 1; } say "HelixScreen: unpacking $(basename "$HELIX_TGZ")" - mkdir -p "$MP/helixscreen" - tar -xzf "$HELIX_TGZ" -C "$MP" # yields $MP/helixscreen/ + mkdir -p "$MOD_PAYLOAD/helixscreen" + tar -xzf "$HELIX_TGZ" -C "$MOD_PAYLOAD" # yields $MOD_PAYLOAD/helixscreen/ # Printer-database entry so it detects the Creator 5 Pro as a tool changer - mkdir -p "$MP/helixscreen/config/printer_database.d" + mkdir -p "$MOD_PAYLOAD/helixscreen/config/printer_database.d" cp -f payload/helixscreen/printer_database.d/*.json \ - "$MP/helixscreen/config/printer_database.d/" + "$MOD_PAYLOAD/helixscreen/config/printer_database.d/" # Optional platform hook. No such file is in the repo, so this never fires # on a stock checkout -- drop one in assets/ to have it shipped. [ -f assets/hooks-creator5.sh ] && \ - cp -f assets/hooks-creator5.sh "$MP/helixscreen/assets/config/platform/" - du -sh "$MP/helixscreen" | awk '{print " "$1}' + cp -f assets/hooks-creator5.sh "$MOD_PAYLOAD/helixscreen/assets/config/platform/" + du -sh "$MOD_PAYLOAD/helixscreen" | awk '{print " "$1}' else skip "HelixScreen" fi @@ -301,7 +301,7 @@ else skip "SSH" fi if [ -n "${BUSYBOX_BIN:-}" ] && [ -f "$BUSYBOX_BIN" ]; then - cp -f "$BUSYBOX_BIN" "$MP/bin/busybox"; chmod +x "$MP/bin/busybox" + cp -f "$BUSYBOX_BIN" "$MOD_PAYLOAD/bin/busybox"; chmod +x "$MOD_PAYLOAD/bin/busybox" fi # --------------------------------------------------------- 7. root password @@ -311,15 +311,15 @@ if [ -n "${ROOT_PW_HASH:-}" ]; then # what dropbear reads at authentication time -- so this is the live shadow # even though dropbear started earlier from the read-only squashfs. awk -v h="$ROOT_PW_HASH" 'BEGIN{FS=OFS=":"} $1=="root"{$2=h} {print}' \ - "$SW/shadow" > "$SW/shadow.new" && mv -f "$SW/shadow.new" "$SW/shadow" + "$SOFTWARE_DIR/shadow" > "$SOFTWARE_DIR/shadow.new" && mv -f "$SOFTWARE_DIR/shadow.new" "$SOFTWARE_DIR/shadow" else skip "root password (set ROOT_PW_HASH)" fi # ------------------------------------------------ 8. start.sh (web stack on) say "start.sh: enabling nginx + moonraker" -cp -f payload/start.sh "$SW/start.sh" -chmod +x "$SW/start.sh" +cp -f payload/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 -> @@ -332,20 +332,20 @@ chmod +x "$SW/start.sh" # here could ever be a reliable backup. Flashing the stock FlashForge package # -- which still ships the binary -- is the uninstall. say "firmwareExe: installing wrapper (replaces the stock binary)" -cp -f payload/firmwareExe "$SW/firmwareExe" -chmod +x "$SW/firmwareExe" +cp -f payload/firmwareExe "$SOFTWARE_DIR/firmwareExe" +chmod +x "$SOFTWARE_DIR/firmwareExe" # ----------------------------------------------------- 10. mod service dir -mkdir -p "$MP/init.d" -[ -d payload/bin ] && cp -f payload/bin/* "$MP/bin/" && chmod +x "$MP/bin"/* -cp -f payload/init.d/S* "$MP/init.d/" -chmod +x "$MP/init.d"/S* +mkdir -p "$MOD_PAYLOAD/init.d" +[ -d payload/bin ] && cp -f payload/bin/* "$MOD_PAYLOAD/bin/" && chmod +x "$MOD_PAYLOAD/bin"/* +cp -f payload/init.d/S* "$MOD_PAYLOAD/init.d/" +chmod +x "$MOD_PAYLOAD/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" + payload/anvil.conf > "$MOD_PAYLOAD/anvil.conf" # --------------------------------------------------- 11. run.sh install step say "run.sh: injecting mod install blocks (pre + post)" @@ -361,7 +361,7 @@ else fi sed -e "s/^MOD_PW_AUTO=.*/MOD_PW_AUTO=$PW_AUTO/" \ payload/run-append.sh > "$POST" -python3 - "$SW/run.sh" payload/run-pre.sh "$POST" <<'PY' +python3 - "$SOFTWARE_DIR/run.sh" payload/run-pre.sh "$POST" <<'PY' import sys, re run, pre_f, post_f = sys.argv[1], sys.argv[2], sys.argv[3] B1, E1 = "# >>> anvil pre >>>", "# <<< anvil pre <<<" @@ -389,11 +389,11 @@ src = src[:j] + post + src[j:] print(" post-block inserted before exit") open(run, 'w', encoding='utf-8', errors='surrogateescape').write(src) PY -chmod +x "$SW/run.sh" +chmod +x "$SOFTWARE_DIR/run.sh" rm -f "$POST" echo echo "Patched." -echo " software component: $(du -sh "$SW" | cut -f1) (-> /usr/prog, firmware partition)" -echo " mod payload: $(du -sh "$MP" | cut -f1) (-> /usr/data/anvil, data partition)" +echo " software component: $(du -sh "$SOFTWARE_DIR" | cut -f1) (-> /usr/prog, firmware partition)" +echo " mod payload: $(du -sh "$MOD_PAYLOAD" | cut -f1) (-> /usr/data/anvil, data partition)" echo "Now run ./bin/pack.sh" diff --git a/bin/preview-boot-screen.py b/bin/preview-boot-screen.py index 2d7ef17..ad2d688 100755 --- a/bin/preview-boot-screen.py +++ b/bin/preview-boot-screen.py @@ -66,24 +66,24 @@ LOGFILE = '/USR/DATA/LOGS/ANVIL-BOOT.LOG' def load_ffscreen(): path = os.path.join(ROOT, 'payload', 'bin', 'ffscreen.py') spec = importlib.util.spec_from_file_location('ffscreen', path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module def png(path, width, height, rgb_rows): def chunk(tag, data): - body = tag + data - return (struct.pack('>I', len(data)) + body - + struct.pack('>I', zlib.crc32(body))) + tagged = tag + data + return (struct.pack('>I', len(data)) + tagged + + struct.pack('>I', zlib.crc32(tagged))) # One filter byte (0 = none) per scanline, then the raw RGB triples. - raw = b''.join(b'\x00' + row for row in rgb_rows) + scanlines = b''.join(b'\x00' + row for row in rgb_rows) with open(path, 'wb') as fh: fh.write(b'\x89PNG\r\n\x1a\n') fh.write(chunk(b'IHDR', struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0))) - fh.write(chunk(b'IDAT', zlib.compress(raw, 6))) + fh.write(chunk(b'IDAT', zlib.compress(scanlines, 6))) fh.write(chunk(b'IEND', b'')) @@ -93,17 +93,17 @@ def to_rows(buf, screen): The buffer is read in its own orientation and then turned by whatever the panel turns it by, so the PNG matches the screen and not the memory. """ - step = screen.bpp // 8 + bytes_per_pixel = screen.bpp // 8 - def pixel(bx, by): - off = by * screen.stride + bx * step - px = buf[off:off + step] - if step == 4: - return bytes((px[2], px[1], px[0])) # B,G,R,X on the wire - v = px[0] | (px[1] << 8) # RGB565 - return bytes((((v >> 11) & 0x1F) << 3, - ((v >> 5) & 0x3F) << 2, - (v & 0x1F) << 3)) + def pixel(buf_x, buf_y): + offset = buf_y * screen.stride + buf_x * bytes_per_pixel + raw = buf[offset:offset + bytes_per_pixel] + if bytes_per_pixel == 4: + return bytes((raw[2], raw[1], raw[0])) # B,G,R,X on the wire + packed = raw[0] | (raw[1] << 8) # RGB565 + return bytes((((packed >> 11) & 0x1F) << 3, + ((packed >> 5) & 0x3F) << 2, + (packed & 0x1F) << 3)) # The inverse of the mapping in ffscreen._rect. rows = [] @@ -121,12 +121,16 @@ def to_rows(buf, screen): def main(argv): - ap = argparse.ArgumentParser(description='render the first-boot screen') - ap.add_argument('--out', default=os.path.join(ROOT, 'work', 'boot-screen')) - ap.add_argument('--size', default='480x800@32', - help='the FRAMEBUFFER, not the panel (default: the real one)') - ap.add_argument('--rotate', type=int, default=None, choices=[0, 90, 270]) - args = ap.parse_args(argv) + parser = argparse.ArgumentParser( + description='render the first-boot screen') + parser.add_argument('--out', + default=os.path.join(ROOT, 'work', 'boot-screen')) + parser.add_argument( + '--size', default='480x800@32', + help='the FRAMEBUFFER, not the panel (default: the real one)') + parser.add_argument('--rotate', type=int, default=None, + choices=[0, 90, 270]) + args = parser.parse_args(argv) ffscreen = load_ffscreen() geometry = ffscreen.parse_geometry(args.size) diff --git a/bin/verify.sh b/bin/verify.sh index 55a68a5..9660117 100755 --- a/bin/verify.sh +++ b/bin/verify.sh @@ -16,25 +16,25 @@ ok() { printf ' \033[32mPASS\033[0m %s\n' "$*"; } bad() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; FAIL=1; } warn() { printf ' \033[33mWARN\033[0m %s\n' "$*"; } -T=$(mktemp -d); trap 'rm -rf "$T"' EXIT +WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT # 1 ---------------------------------------------------------------- decrypt if openssl des3 -d -k "$FF_KEY" -salt -md md5 -in "$PKG" 2>/dev/null \ - | tar -xf - -C "$T" 2>/dev/null; then + | tar -xf - -C "$WORK" 2>/dev/null; then ok "decrypts with the firmware key and is a valid tar" else bad "decrypt/untar failed -- the printer's unTar will reject this"; exit 1 fi # 2 ------------------------------------------------------- installer present -if [ -f "$T/runFirmwareExe.sh" ]; then +if [ -f "$WORK/runFirmwareExe.sh" ]; then ok "runFirmwareExe.sh present" else bad "no runFirmwareExe.sh -- app_startup.sh falls back to plain tar and gives up" fi # 3 ----------------------------------------- components are PLAIN tar, not xz -for f in "$T"/software-*.tar.xz "$T"/kernel-*.tar.xz "$T"/control-*.tar.xz "$T"/library-*.tar.xz; do +for f in "$WORK"/software-*.tar.xz "$WORK"/kernel-*.tar.xz "$WORK"/control-*.tar.xz "$WORK"/library-*.tar.xz; do [ -f "$f" ] || continue b=$(basename "$f") if tar -tf "$f" >/dev/null 2>&1; then @@ -45,38 +45,38 @@ for f in "$T"/software-*.tar.xz "$T"/kernel-*.tar.xz "$T"/control-*.tar.xz "$T"/ done # 4 ------------------------------------------------------------- md5sum gate -SW=$(ls -1 "$T"/software-*.tar.xz 2>/dev/null | head -n1) -if [ -n "$SW" ]; then - mkdir -p "$T/sw" && tar -xf "$SW" -C "$T/sw" - if [ -f "$T/sw/md5sum.list" ]; then +SOFTWARE_TAR=$(ls -1 "$WORK"/software-*.tar.xz 2>/dev/null | head -n1) +if [ -n "$SOFTWARE_TAR" ]; then + mkdir -p "$WORK/sw" && tar -xf "$SOFTWARE_TAR" -C "$WORK/sw" + if [ -f "$WORK/sw/md5sum.list" ]; then # NB: the printer's busybox md5sum takes -s for quiet; GNU coreutils # spells it --status. Use whichever this host has. - if ( cd "$T/sw" && md5sum --status -c md5sum.list 2>/dev/null ) \ - || ( cd "$T/sw" && md5sum -s -c md5sum.list 2>/dev/null ); then - ok "md5sum.list verifies ($(wc -l < "$T/sw/md5sum.list") entries)" + if ( cd "$WORK/sw" && md5sum --status -c md5sum.list 2>/dev/null ) \ + || ( cd "$WORK/sw" && md5sum -s -c md5sum.list 2>/dev/null ); then + ok "md5sum.list verifies ($(wc -l < "$WORK/sw/md5sum.list") entries)" else bad "md5sum.list does NOT verify -- installer aborts and deletes the payload" - ( cd "$T/sw" && md5sum -c md5sum.list 2>&1 | grep -v ': OK$' | head -5 | sed 's/^/ /' ) + ( cd "$WORK/sw" && md5sum -c md5sum.list 2>&1 | grep -v ': OK$' | head -5 | sed 's/^/ /' ) fi - grep -q 'md5sum.list' "$T/sw/md5sum.list" && bad "md5sum.list lists itself (can never match)" + grep -q 'md5sum.list' "$WORK/sw/md5sum.list" && bad "md5sum.list lists itself (can never match)" else bad "no md5sum.list in the software component" fi fi # 5 -------------------------------------------------- shell syntax of scripts -for s in "$T/runFirmwareExe.sh" "$T/sw/run.sh" "$T/sw/start.sh" "$T/sw/app_startup.sh"; do +for s in "$WORK/runFirmwareExe.sh" "$WORK/sw/run.sh" "$WORK/sw/start.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 done # 6 ---------------------------------------------- boot chain actually hooks up -if [ -f "$T/sw/app_startup.sh" ]; then - grep -q 'gt9xx_touch.ko' "$T/sw/app_startup.sh" \ +if [ -f "$WORK/sw/app_startup.sh" ]; then + grep -q 'gt9xx_touch.ko' "$WORK/sw/app_startup.sh" \ && ok "touchscreen driver still loaded at boot" \ || bad "gt9xx_touch.ko insmod missing -- no touchscreen" - grep -q 'firmwareExe' "$T/sw/app_startup.sh" \ + grep -q 'firmwareExe' "$WORK/sw/app_startup.sh" \ && ok "a UI is launched at boot" \ || bad "nothing launches a UI -- printer boots to a blank screen" fi @@ -86,23 +86,23 @@ fi # the v20260824 release passed here precisely because everything below was # conditional on files that the broken build simply did not contain. if [ "${BUILD_KLIPPER:-fork}" = "fork" ]; then - if [ -f "$T/sw/klipper/klippy/chelper/__init__.py" ]; then + if [ -f "$WORK/sw/klipper/klippy/chelper/__init__.py" ]; then ok "package carries the fork klippy tree" else bad "BUILD_KLIPPER=fork but no klippy tree in the package -- this is the stock-overlay build that shipped as v20260824" fi - [ -f "$T/sw/klipper/chelper.tar" ] \ + [ -f "$WORK/sw/klipper/chelper.tar" ] \ || bad "BUILD_KLIPPER=fork but no chelper.tar in the package" fi -if [ -f "$T/sw/klipper/chelper.tar" ]; then - mkdir -p "$T/ch" && tar -xf "$T/sw/klipper/chelper.tar" -C "$T/ch" - CH=$(find "$T/ch" -name 'c_helper.so' | head -n1) - if [ -n "$CH" ] && ! head -c 4 "$CH" | grep -q ELF; then +if [ -f "$WORK/sw/klipper/chelper.tar" ]; then + mkdir -p "$WORK/ch" && tar -xf "$WORK/sw/klipper/chelper.tar" -C "$WORK/ch" + CHELPER=$(find "$WORK/ch" -name 'c_helper.so' | head -n1) + if [ -n "$CHELPER" ] && ! head -c 4 "$CHELPER" | grep -q ELF; then warn "c_helper.so is not an ELF (synthetic fixture) -- ABI check skipped" - CH="" + CHELPER="" fi - if [ -n "$CH" ]; then - if readelf -h "$CH" 2>/dev/null | grep -q nan2008; then + if [ -n "$CHELPER" ]; then + if readelf -h "$CHELPER" 2>/dev/null | grep -q nan2008; then ok "c_helper.so is nan2008 MIPS32r2 (kernel will load it)" else bad "c_helper.so is NOT nan2008 -- kernel returns ENOEXEC, klippy dies" @@ -110,8 +110,8 @@ if [ -f "$T/sw/klipper/chelper.tar" ]; then # The failure mode that reached a printer: a .so from older sources # than the klippy tree beside it. cffi resolves lazily, so only a # symbol-table check catches it before the machine does. - if [ -f "$T/sw/klipper/klippy/chelper/__init__.py" ]; then - if python3 "$ROOT/test/test-chelper.py" "$T/sw/klipper" >/dev/null 2>&1; then + if [ -f "$WORK/sw/klipper/klippy/chelper/__init__.py" ]; then + if python3 "$ROOT/test/test-chelper.py" "$WORK/sw/klipper" >/dev/null 2>&1; then ok "c_helper.so exports everything the shipped klippy declares" else bad "c_helper.so does not match the shipped klippy tree (stale build)" @@ -121,12 +121,12 @@ if [ -f "$T/sw/klipper/chelper.tar" ]; then fi # 8 ------------------------------------------------------------ mod payload -if [ -f "$T/anvil.tar.xz" ]; then - if xz -t "$T/anvil.tar.xz" 2>/dev/null; then ok "anvil.tar.xz is valid xz" - elif tar -tf "$T/anvil.tar.xz" >/dev/null 2>&1; then ok "anvil.tar.xz is a plain tar" +if [ -f "$WORK/anvil.tar.xz" ]; then + if xz -t "$WORK/anvil.tar.xz" 2>/dev/null; then ok "anvil.tar.xz is valid xz" + elif tar -tf "$WORK/anvil.tar.xz" >/dev/null 2>&1; then ok "anvil.tar.xz is a plain tar" else bad "anvil.tar.xz is neither valid xz nor tar"; fi - LIST=$(xz -dc "$T/anvil.tar.xz" 2>/dev/null | tar -tf - 2>/dev/null || tar -tf "$T/anvil.tar.xz" 2>/dev/null) + LIST=$(xz -dc "$WORK/anvil.tar.xz" 2>/dev/null | tar -tf - 2>/dev/null || tar -tf "$WORK/anvil.tar.xz" 2>/dev/null) # Herestrings, NOT `echo "$LIST" | grep -q`: under pipefail, grep -q # exiting at an early match SIGPIPEs the echo and the pipeline returns # 141 -- a race that stayed invisible until the fork klippy tree tripled @@ -158,8 +158,8 @@ fi # The most easily missed failure: a package built from the wrong model's # firmware refuses to install, and a package built from the RIGHT model but # flashed to the wrong one would install the wrong binaries. -PKG_MACHINE=$(sed -n 's/^MACHINE=//p' "$T/runFirmwareExe.sh" 2>/dev/null | head -n1) -PKG_PID=$(sed -n 's/^PID=//p' "$T/runFirmwareExe.sh" 2>/dev/null | head -n1) +PKG_MACHINE=$(sed -n 's/^MACHINE=//p' "$WORK/runFirmwareExe.sh" 2>/dev/null | head -n1) +PKG_PID=$(sed -n 's/^PID=//p' "$WORK/runFirmwareExe.sh" 2>/dev/null | head -n1) if [ -n "$PKG_MACHINE" ]; then echo " package installs on: $PKG_MACHINE (PID $PKG_PID)" if [ -n "${TARGET_MACHINE:-}" ]; then @@ -182,11 +182,11 @@ fi # it: the Pro looks for /mnt/Creator5Pro-*.tgz, the non-Pro for # /mnt/Creator5-*.tgz. It must ALSO match the gate inside, or the printer # picks the file up and then refuses it. -BN=$(basename "$PKG") -case "$BN" in +PKG_NAME=$(basename "$PKG") +case "$PKG_NAME" in Creator5Pro-*.tgz) FN_MACHINE=Creator5Pro ;; Creator5-*.tgz) FN_MACHINE=Creator5 ;; - *) FN_MACHINE=""; bad "filename '$BN' matches no glob -- app_startup.sh will ignore it" ;; + *) FN_MACHINE=""; bad "filename '$PKG_NAME' matches no glob -- app_startup.sh will ignore it" ;; esac if [ -n "$FN_MACHINE" ]; then if [ -z "$PKG_MACHINE" ] || [ "$FN_MACHINE" = "$PKG_MACHINE" ]; then @@ -204,24 +204,24 @@ fi # It compares by CONTENT, not by name: a name blacklist would trip over any # file Mainsail or HelixScreen happens to call common.sh, while a byte-for-byte # match against a host-lane file is exactly the thing we are trying to catch. -mkdir -p "$T/ship" -tar -xf "$T"/software-*.tar.xz -C "$T/ship" 2>/dev/null || true -if [ -f "$T/anvil.tar.xz" ]; then - mkdir -p "$T/ship/.anvil" - xz -dc "$T/anvil.tar.xz" 2>/dev/null | tar -xf - -C "$T/ship/.anvil" 2>/dev/null \ - || tar -xf "$T/anvil.tar.xz" -C "$T/ship/.anvil" 2>/dev/null || true +mkdir -p "$WORK/ship" +tar -xf "$WORK"/software-*.tar.xz -C "$WORK/ship" 2>/dev/null || true +if [ -f "$WORK/anvil.tar.xz" ]; then + mkdir -p "$WORK/ship/.anvil" + xz -dc "$WORK/anvil.tar.xz" 2>/dev/null | tar -xf - -C "$WORK/ship/.anvil" 2>/dev/null \ + || tar -xf "$WORK/anvil.tar.xz" -C "$WORK/ship/.anvil" 2>/dev/null || true fi find bin test docker -type f 2>/dev/null | xargs -r md5sum 2>/dev/null \ - | awk '{print $1}' | sort -u > "$T/host.md5" -find "$T/ship" -type f 2>/dev/null | xargs -r md5sum 2>/dev/null > "$T/ship.md5" -LEAK=$(awk 'NR==FNR{h[$1];next} $1 in h {print}' "$T/host.md5" "$T/ship.md5" \ - | sed "s|$T/ship/||") + | awk '{print $1}' | sort -u > "$WORK/host.md5" +find "$WORK/ship" -type f 2>/dev/null | xargs -r md5sum 2>/dev/null > "$WORK/ship.md5" +LEAK=$(awk 'NR==FNR{h[$1];next} $1 in h {print}' "$WORK/host.md5" "$WORK/ship.md5" \ + | sed "s|$WORK/ship/||") # Those finds are RELATIVE, so from any cwd but the repo root host.md5 comes # out empty, the join matches nothing and this reports ok having compared # nothing -- retiring the only check that keeps config.env and the host tooling # off a USB stick. common.sh cds to the root, so this is a guard against a # future caller that does not. -if [ ! -s "$T/host.md5" ]; then +if [ ! -s "$WORK/host.md5" ]; then bad "ship-boundary check compared nothing (no files found under bin/, test/, docker/ -- wrong cwd?)" elif [ -z "$LEAK" ]; then ok "package carries nothing from bin/, test/ or docker/" diff --git a/payload/bin/ff-startup.py b/payload/bin/ff-startup.py index 96af1a8..1a52d4f 100755 --- a/payload/bin/ff-startup.py +++ b/payload/bin/ff-startup.py @@ -204,17 +204,17 @@ class Moonraker: def __init__(self, base): self.base = base.rstrip('/') - def _open(self, req, timeout): - return urllib.request.urlopen(req, timeout=timeout) + def _open(self, request, timeout): + return urllib.request.urlopen(request, timeout=timeout) def get(self, path, timeout=5.0): """Return an endpoint's "result" object, or None if it is not there yet. Every failure mode -- connection refused, a timeout, an HTTP error, a body that is not JSON -- means the same thing to us (not ready), so they collapse into one None.""" - req = urllib.request.Request(self.base + path, method='GET') + request = urllib.request.Request(self.base + path, method='GET') try: - with self._open(req, timeout) as fh: + with self._open(request, timeout) as fh: body = fh.read() except Exception: return None @@ -230,12 +230,12 @@ class Moonraker: request open, so a timeout or a dropped connection here is not an error -- it is the expected shape of success. The caller decides; we only report what happened.""" - data = json.dumps({'script': script}).encode('utf-8') - req = urllib.request.Request( - self.base + '/printer/gcode/script', data=data, + payload = json.dumps({'script': script}).encode('utf-8') + request = urllib.request.Request( + self.base + '/printer/gcode/script', data=payload, headers={'Content-Type': 'application/json'}, method='POST') try: - with self._open(req, timeout) as fh: + with self._open(request, timeout) as fh: fh.read() return True, 'ok' except urllib.error.HTTPError as exc: @@ -253,30 +253,33 @@ class Moonraker: # -- the two questions we ask about klipper's state -------------------- def klippy_state(self): - info = self.get('/server/info') - if info is None: + server_info = self.get('/server/info') + if server_info is None: return None - return info.get('klippy_state') + return server_info.get('klippy_state') def tools(self): """[ff_tool n] status for every tool, as a list of dicts (or None).""" - query = '&'.join('ff_tool%%20%d' % n for n in range(EXTRUDER_COUNT)) - res = self.get('/printer/objects/query?' + query) - if res is None: + query = '&'.join('ff_tool%%20%d' % index + for index in range(EXTRUDER_COUNT)) + result = self.get('/printer/objects/query?' + query) + if result is None: return None - status = res.get('status', {}) - return [status.get('ff_tool %d' % n) for n in range(EXTRUDER_COUNT)] + status = result.get('status', {}) + return [status.get('ff_tool %d' % index) + for index in range(EXTRUDER_COUNT)] def save_pending(self): - res = self.get('/printer/objects/query?configfile=save_config_pending') - if res is None: + result = self.get( + '/printer/objects/query?configfile=save_config_pending') + if result is None: return None - cf = res.get('status', {}).get('configfile', {}) - return bool(cf.get('save_config_pending')) + configfile = result.get('status', {}).get('configfile', {}) + return bool(configfile.get('save_config_pending')) def any_calibrated(tools): - return any(t and t.get('calibrated') for t in tools or []) + return any(tool and tool.get('calibrated') for tool in tools or []) def board_name(key): @@ -294,20 +297,20 @@ def hand_over_boards(panel, timeout): """ if bringup is None: return - def progress(devs): - if not devs: + def progress(devices): + if not devices: return - names = [board_name(d) for d in devs] + names = [board_name(device) for device in devices] panel.say('WAKING ' + ' AND '.join(names[:2]), 0.08) panel.say('WAKING THE TOOLHEAD BOARDS', 0.05) try: - ok = bringup.bringup(list(bringup.DEFAULT_PORTS), timeout, - on_progress=progress) + all_awake = bringup.bringup(list(bringup.DEFAULT_PORTS), timeout, + on_progress=progress) except Exception as exc: # A board is klippy's problem to report; ours is to not die here. log('mcu bring-up raised (%s) -- going on to klipper' % exc) return - log('mcu bring-up %s' % ('ok' if ok else 'reported a problem')) + log('mcu bring-up %s' % ('ok' if all_awake else 'reported a problem')) # --------------------------------------------------------------- klipper @@ -318,7 +321,7 @@ def klippy_running(): if os.path.exists(UDS): return True try: - pids = [p for p in os.listdir('/proc') if p.isdigit()] + pids = [entry for entry in os.listdir('/proc') if entry.isdigit()] except OSError: return False for pid in pids: @@ -344,16 +347,17 @@ def start_klipper(args): env['FF_SKIP_MCU_BRINGUP'] = '1' log('starting klipper (%s)' % args.start_sh) try: - proc = subprocess.run(['/bin/sh', args.start_sh], env=env, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, timeout=120) + completed = subprocess.run(['/bin/sh', args.start_sh], env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=120) except subprocess.TimeoutExpired: log('start.sh did not return within 120s') return False except Exception as exc: log('start.sh could not be run (%s)' % exc) return False - for line in (proc.stdout or b'').decode('utf-8', 'replace').splitlines(): + for line in (completed.stdout + or b'').decode('utf-8', 'replace').splitlines(): log(' start.sh: %s' % line) return True @@ -382,29 +386,29 @@ def stop_klipper(args): pass -def klippy_fault(mr): +def klippy_fault(moonraker): """The board klippy is complaining about, if it names one. On a failed connect klippy's state_message is "MCU 'eheaterboard' error during connect ..." -- the only place the guilty board is named, and exactly what an owner needs to know.""" - info = mr.get('/printer/info') - if not info: + printer_info = moonraker.get('/printer/info') + if not printer_info: return None - message = info.get('state_message') or '' + message = printer_info.get('state_message') or '' for key in BOARDS: if "'%s'" % key in message: return board_name(key) return None -def report_stack_failure(mr, panel): +def report_stack_failure(moonraker, panel): """The one frame someone is left looking at, naming what did not come up.""" - state = mr.klippy_state() + state = moonraker.klippy_state() if state is None: panel.failed('MOONRAKER IS NOT RESPONDING') elif state == 'error': - board = klippy_fault(mr) + board = klippy_fault(moonraker) if board: panel.failed('%s DID NOT ANSWER' % board) else: @@ -414,24 +418,24 @@ def report_stack_failure(mr, panel): % str(state).upper()) -def wait_for_stack(mr, panel, deadline, started, quiet=False): +def wait_for_stack(moonraker, panel, deadline, started, quiet=False): """Block until klipper and moonraker are both up, or time runs out. Reports their state once per change so the boot log says which one everybody is waiting on, and creeps the panel's bar along with the wait so a long MCU hunt still looks like progress rather than a freeze.""" - last = None - span = max(1.0, deadline - started) + last_state = None + window = max(1.0, deadline - started) while True: - state = mr.klippy_state() - if state != last: + state = moonraker.klippy_state() + if state != last_state: log('waiting: moonraker=%s klipper=%s' % ('up' if state is not None else 'down', state or 'unknown')) - last = state + last_state = state + elapsed_fraction = 0.3 * (time.time() - started) / window if state is None: - panel.say('STARTING SERVICES', 0.05 + 0.3 * (time.time() - started) / span) + panel.say('STARTING SERVICES', 0.05 + elapsed_fraction) elif state != 'ready': - panel.say('WAITING FOR THE PRINTER', - 0.05 + 0.3 * (time.time() - started) / span) + panel.say('WAITING FOR THE PRINTER', 0.05 + elapsed_fraction) if state == 'ready': return True if state == 'error' and quiet: @@ -448,16 +452,16 @@ def wait_for_stack(mr, panel, deadline, started, quiet=False): # replaced two seconds later by another attempt reads as a # failure the printer then ignored. if not quiet: - report_stack_failure(mr, panel) + report_stack_failure(moonraker, panel) return False time.sleep(2.0) -def wait_for_ready(mr, deadline): +def wait_for_ready(moonraker, deadline): """Wait for klippy alone -- used after the SAVE_CONFIG restart, where moonraker never went away and mainsail was never in question.""" while True: - if mr.klippy_state() == 'ready': + if moonraker.klippy_state() == 'ready': return True if time.time() >= deadline: return False @@ -485,17 +489,17 @@ def stamp(path, note): def run(args): started = time.time() deadline = started + args.timeout - mr = Moonraker(args.moonraker) + moonraker = Moonraker(args.moonraker) panel = Panel(args.fb, args.fb_geometry, enabled=not args.no_screen) panel.say('STARTING SERVICES', 0.05) try: - return startup(args, mr, panel, started, deadline) + return startup(args, moonraker, panel, started, deadline) finally: # Whatever happened, hand HelixScreen a clean panel. panel.done() -def bring_up_printer(args, mr, panel, started, deadline): +def bring_up_printer(args, moonraker, panel, started, deadline): """Boards out of their bootloaders, klippy up, and retry until it is. This is what init.d/S70klipper's supervise() used to do by tailing @@ -510,27 +514,27 @@ def bring_up_printer(args, mr, panel, started, deadline): if not args.no_klipper and not start_klipper(args): panel.failed('KLIPPER COULD NOT BE STARTED') return False - if wait_for_stack(mr, panel, deadline, started, quiet=True): + if wait_for_stack(moonraker, panel, deadline, started, quiet=True): return True if time.time() >= deadline or attempt == args.klipper_tries: break # Reopening the port toggles DTR/RTS, which is what gives a board # that missed its window another chance -- so the restart is the # fix, not merely another go at the same thing. - board = klippy_fault(mr) + board = klippy_fault(moonraker) log('attempt %d/%d failed (%s) -- restarting klipper' % (attempt, args.klipper_tries, board or 'no board named')) panel.say('RETRYING %s' % (board or 'THE PRINTER'), 0.15, detail='ATTEMPT %d OF %d' % (attempt + 1, args.klipper_tries)) stop_klipper(args) - report_stack_failure(mr, panel) + report_stack_failure(moonraker, panel) return False -def startup(args, mr, panel, started, deadline): +def startup(args, moonraker, panel, started, deadline): # -- every boot ------------------------------------------------------ - if not bring_up_printer(args, mr, panel, started, deadline): + if not bring_up_printer(args, moonraker, panel, started, deadline): return 1 # -- first boot only ------------------------------------------------- @@ -546,12 +550,12 @@ def startup(args, mr, panel, started, deadline): return 0 log('first boot: importing this unit\'s factory calibration from %s' % args.dir) - return migrate(args, mr, panel) + return migrate(args, moonraker, panel) -def migrate(args, mr, panel): +def migrate(args, moonraker, panel): deadline = time.time() + args.timeout - tools = mr.tools() + tools = moonraker.tools() if tools is None: log('could not read the ff_tool objects -- no stamp') panel.failed('COULD NOT READ THE TOOL SETTINGS') @@ -564,7 +568,7 @@ def migrate(args, mr, panel): stamp(args.stamp, 'already calibrated; nothing imported') return 0 - pending = mr.save_pending() + pending = moonraker.save_pending() if pending is None: log('could not read save_config_pending -- no stamp') panel.failed('COULD NOT READ THE PRINTER CONFIGURATION') @@ -581,12 +585,12 @@ def migrate(args, mr, panel): return 0 panel.say('READING FACTORY CALIBRATION', 0.5) - ok, detail = mr.gcode('FF_IMPORT_FIRMWARE_CONFIG') - if not ok: + applied, detail = moonraker.gcode('FF_IMPORT_FIRMWARE_CONFIG') + if not applied: log('FF_IMPORT_FIRMWARE_CONFIG failed: %s' % detail) panel.failed('THE PRINTER REFUSED FF_IMPORT_FIRMWARE_CONFIG') return 1 - tools = mr.tools() + tools = moonraker.tools() if not any_calibrated(tools): # The command ran but the JSON held nothing usable. Saving now would # persist nothing and restart klippy for no reason. @@ -596,17 +600,17 @@ def migrate(args, mr, panel): log('imported; persisting with SAVE_CONFIG -- klipper restarts once') panel.say('SAVING CALIBRATION', 0.7) - ok, detail = mr.gcode('SAVE_CONFIG', timeout=20.0) - if not ok: + applied, detail = moonraker.gcode('SAVE_CONFIG', timeout=20.0) + if not applied: # Expected: the restart cuts the connection before moonraker answers. log('SAVE_CONFIG did not answer (%s) -- klipper is restarting' % detail) panel.say('RESTARTING THE PRINTER', 0.85) - if not wait_for_ready(mr, max(deadline, time.time() + 90.0)): + if not wait_for_ready(moonraker, max(deadline, time.time() + 90.0)): log('klipper did not come back after SAVE_CONFIG -- no stamp') panel.failed('KLIPPER DID NOT RESTART AFTER SAVING') return 1 - tools = mr.tools() + tools = moonraker.tools() if not any_calibrated(tools): log('klipper came back with no saved nozzle position -- the save did' ' not take, no stamp') @@ -629,50 +633,51 @@ def selftest(): the script running itself, which is what this is for -- the replica gate calls it, and it is worth having on a printer over ssh too. """ - ok = True + passed = True print('ffscreen: %s' % ('yes' if ffscreen is not None else 'NO')) if ffscreen is None: - ok = False + passed = False print('ff_mcu_bringup: %s' % ('yes' if bringup is not None else 'NO')) if bringup is None: - ok = False + passed = False else: ports = list(bringup.DEFAULT_PORTS) print('ports: %d (%s)' % (len(ports), ' '.join(ports))) - unnamed = [d for d in ports if not board_name(d).startswith('THE ')] + unnamed = [port for port in ports + if not board_name(port).startswith('THE ')] print('named: %s' % ('yes' if not unnamed else 'NO %s' % unnamed)) if unnamed: - ok = False - print('selftest: %s' % ('ok' if ok else 'FAILED')) - return 0 if ok else 1 + passed = False + print('selftest: %s' % ('ok' if passed else 'FAILED')) + return 0 if passed else 1 def main(argv): - ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument('--stamp', default=STAMP) - ap.add_argument('--dir', default=JSON_DIR) - ap.add_argument('--moonraker', default=MOONRAKER) - ap.add_argument('--timeout', type=float, default=TIMEOUT) - ap.add_argument('--mcu-timeout', type=float, default=MCU_TIMEOUT, - help="how long one bring-up pass may take") - ap.add_argument('--klipper-tries', type=int, default=KLIPPER_TRIES, - help="how many times to hand the boards over and retry") - ap.add_argument('--start-sh', default=START_SH) - ap.add_argument('--daemon', default=DAEMON) - ap.add_argument('--no-klipper', action='store_true', - help="do not start klipper; only wait for it") - ap.add_argument('--no-import', action='store_true', - help="only bring the printer up; never migrate") - ap.add_argument('--fb', default=None, - help='framebuffer to draw the boot screen on') - ap.add_argument('--fb-geometry', default=None, - help="panel size as WxH@BPP, skipping the sysfs probe") - ap.add_argument('--no-screen', action='store_true', - help='do not draw anything on the panel') - ap.add_argument('--dry-run', action='store_true') - ap.add_argument('--selftest', action='store_true', - help='report what this script can reach, and exit') - args = ap.parse_args(argv) + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--stamp', default=STAMP) + parser.add_argument('--dir', default=JSON_DIR) + parser.add_argument('--moonraker', default=MOONRAKER) + parser.add_argument('--timeout', type=float, default=TIMEOUT) + parser.add_argument('--mcu-timeout', type=float, default=MCU_TIMEOUT, + help="how long one bring-up pass may take") + parser.add_argument('--klipper-tries', type=int, default=KLIPPER_TRIES, + help="how many times to hand the boards over and retry") + parser.add_argument('--start-sh', default=START_SH) + parser.add_argument('--daemon', default=DAEMON) + parser.add_argument('--no-klipper', action='store_true', + help="do not start klipper; only wait for it") + parser.add_argument('--no-import', action='store_true', + help="only bring the printer up; never migrate") + parser.add_argument('--fb', default=None, + help='framebuffer to draw the boot screen on') + parser.add_argument('--fb-geometry', default=None, + help="panel size as WxH@BPP, skipping the sysfs probe") + parser.add_argument('--no-screen', action='store_true', + help='do not draw anything on the panel') + parser.add_argument('--dry-run', action='store_true') + parser.add_argument('--selftest', action='store_true', + help='report what this script can reach, and exit') + args = parser.parse_args(argv) if args.selftest: return selftest() try: diff --git a/payload/bin/ff_mcu_bringup.py b/payload/bin/ff_mcu_bringup.py index a388183..161ab9a 100755 --- a/payload/bin/ff_mcu_bringup.py +++ b/payload/bin/ff_mcu_bringup.py @@ -102,7 +102,8 @@ def configure(fd): VMIN=0 with VTIME=1 is what makes every read return after 0.1s whether or not the board said anything, which is what paces the poll below. """ - iflag, oflag, cflag, lflag, _ispeed, _ospeed, cc = termios.tcgetattr(fd) + (iflag, oflag, cflag, lflag, + _ispeed, _ospeed, control_chars) = termios.tcgetattr(fd) iflag &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK | termios.ISTRIP | termios.INLCR | termios.IGNCR @@ -117,26 +118,26 @@ def configure(fd): # just started. cflag &= ~termios.HUPCL - cc = list(cc) - cc[termios.VMIN] = 0 - cc[termios.VTIME] = 1 + control_chars = list(control_chars) + control_chars[termios.VMIN] = 0 + control_chars[termios.VTIME] = 1 termios.tcsetattr(fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, - termios.B115200, termios.B115200, cc]) + termios.B115200, termios.B115200, control_chars]) class Port(object): """One serial port, held open for the whole pass.""" - def __init__(self, dev): - self.dev = dev + def __init__(self, device): + self.device = device self.fd = None self.done = False # no more work to do on this port self.verdict = None # what to print at the end self.ok = True # counts towards the exit status self.banners = 0 - self.gos = 0 # how many 'A' have gone out + self.a_sent = 0 # how many 'A' have gone out self.acks = [] self.bytes_seen = 0 self.last_go = None # when 'A' was last sent @@ -146,21 +147,21 @@ class Port(object): def open(self): try: - self.fd = os.open(self.dev, os.O_RDWR | os.O_NOCTTY) - except OSError as e: - self.fail("%s" % e.strerror) + self.fd = os.open(self.device, os.O_RDWR | os.O_NOCTTY) + except OSError as err: + self.fail("%s" % err.strerror) return False try: configure(self.fd) - except termios.error as e: - self.fail("cannot configure: %s" % e) + except termios.error as err: + self.fail("cannot configure: %s" % err) return False return True - def fail(self, msg): + def fail(self, reason): self.done = True self.ok = False - self.verdict = msg + self.verdict = reason self.close() def close(self): @@ -171,8 +172,8 @@ class Port(object): def read(self, size=64): try: return os.read(self.fd, size) - except OSError as e: - self.fail("read failed: %s" % e.strerror) + except OSError as err: + self.fail("read failed: %s" % err.strerror) return None def poll(self, now): @@ -182,8 +183,8 @@ class Port(object): return if data: self.bytes_seen += len(data) - buf = self.tail + data - self.tail = buf[-(len(BANNER) - 1):] if len(BANNER) > 1 else b"" + seen = self.tail + data + self.tail = seen[-(len(BANNER) - 1):] if len(BANNER) > 1 else b"" # Deliberately stricter than stock. firmwareExe only requires a # literal "Ready" on the heat board; on ttyS5 and ttyS7 its ready @@ -191,7 +192,7 @@ class Port(object): # on the strength of that. We will not: a board that is already # running is speaking the Klipper protocol, and 'A' has no business # on that wire. The banner is the only thing that earns a write. - if BANNER in buf: + if BANNER in seen: self.banners += 1 self.handshake(now) return @@ -203,11 +204,11 @@ class Port(object): self.verdict = ("started its application (ack 0x%02x, not " "0x%02x -- but the banner stopped after %d " "'A', so it did leave the bootloader)" - % (ack, GOOD_ACK, self.gos)) + % (ack, GOOD_ACK, self.a_sent)) else: self.verdict = ("no ack to %d 'A', but the banner stopped " "-- assuming it left the bootloader" - % self.gos) + % self.a_sent) self.done = True def handshake(self, now): @@ -250,7 +251,7 @@ class Port(object): self.last_go = now for _ in range(ACK_TRIES): os.write(self.fd, GO) - self.gos += 1 + self.a_sent += 1 # Each read is one VTIME tick, so this paces itself at ~10 'A'/s # and the whole loop is bounded by 50 * 0.1s = 5s. ack = self.read(1) @@ -261,7 +262,7 @@ class Port(object): if ack[0] == GOOD_ACK: self.verdict = ("started its application (ack 0x%02x " "after %d 'A', %d banner(s))" - % (ack[0], self.gos, self.banners)) + % (ack[0], self.a_sent, self.banners)) self.done = True return # Drop anything left over from the banner so the next poll does not @@ -277,7 +278,7 @@ class Port(object): ack = self.acks[-1] if self.acks else None self.verdict = ("still bannering after %d 'A' (%d banners, last " "ack %s) -- did NOT leave the bootloader" - % (self.gos, self.banners, + % (self.a_sent, self.banners, "0x%02x" % ack if ack is not None else "none")) self.ok = False elif self.bytes_seen: @@ -291,47 +292,48 @@ class Port(object): self.done = True -def bringup(devs, deadline_s=DEADLINE_S, on_progress=None): +def bringup(devices, deadline_s=DEADLINE_S, on_progress=None): """Hand every board over, in one pass. - on_progress(devs_still_working) is called whenever that set shrinks, so a + on_progress(devices_still_working) is called whenever that set + shrinks, so a caller can say which board is being waited for. It is called from inside the poll loop and must be cheap; anything it raises is the caller's problem to have avoided, because a bring-up must not fail over a progress note. """ - ports = [Port(d) for d in devs] - live = [p for p in ports if p.open()] + ports = [Port(device) for device in devices] + live = [port for port in ports if port.open()] try: - end = time.monotonic() + deadline_s + deadline = time.monotonic() + deadline_s waiting = None while True: - pending = [p for p in live if not p.done] - if not pending or time.monotonic() >= end: + pending = [port for port in live if not port.done] + if not pending or time.monotonic() >= deadline: break # Only when the set actually shrinks: this loop spins hard, and # calling back on every pass would be pure noise. - names = tuple(p.dev for p in pending) + names = tuple(port.device for port in pending) if names != waiting: waiting = names if on_progress is not None: on_progress(list(names)) - for p in pending: - p.poll(time.monotonic()) - for p in live: - p.finish(deadline_s) + for port in pending: + port.poll(time.monotonic()) + for port in live: + port.finish(deadline_s) finally: - for p in live: - p.close() + for port in live: + port.close() if on_progress is not None: on_progress([]) - ok = True - for p in ports: - print("mcu-bringup: %s %s" % (p.dev, p.verdict)) - if not p.ok: - ok = False - return ok + all_ok = True + for port in ports: + print("mcu-bringup: %s %s" % (port.device, port.verdict)) + if not port.ok: + all_ok = False + return all_ok def main(argv): diff --git a/payload/bin/ffscreen.py b/payload/bin/ffscreen.py index e8a02fe..f8e4ada 100644 --- a/payload/bin/ffscreen.py +++ b/payload/bin/ffscreen.py @@ -155,9 +155,9 @@ class Screen: self.stride = 0 else: try: - xres, yres = _read( + width_text, height_text = _read( os.path.join(sysfs, 'virtual_size')).split(',') - self.buf_w, self.buf_h = int(xres), int(yres) + self.buf_w, self.buf_h = int(width_text), int(height_text) self.bpp = int(_read(os.path.join(sysfs, 'bits_per_pixel'))) except Exception: return @@ -193,8 +193,8 @@ class Screen: if self.bpp == 32: # The usual Linux packing: B, G, R, then an ignored byte. return bytes((b, g, r, 0xFF)) - v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) - return bytes((v & 0xFF, v >> 8)) + packed = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) + return bytes((packed & 0xFF, packed >> 8)) def _blank(self): row = self._pixel(BACKGROUND) * self.buf_w @@ -214,22 +214,22 @@ class Screen: w, h = x1 - x0, y1 - y0 if self.rotate == 90: # display(x, y) lives at buffer(y, buf_h - 1 - x) - bx, by, bw, bh = y0, self.buf_h - x0 - w, h, w + dest_x, dest_y, dest_w, dest_h = y0, self.buf_h - x0 - w, h, w elif self.rotate == 270: - bx, by, bw, bh = self.buf_w - y0 - h, x0, h, w + dest_x, dest_y, dest_w, dest_h = self.buf_w - y0 - h, x0, h, w else: - bx, by, bw, bh = x0, y0, w, h + dest_x, dest_y, dest_w, dest_h = x0, y0, w, h - px = self._pixel(rgb) - span = px * bw - step = self.bpp // 8 - for row in range(by, by + bh): - start = row * self.stride + bx * step - buf[start:start + len(span)] = span + pixel = self._pixel(rgb) + row_bytes = pixel * dest_w + bytes_per_pixel = self.bpp // 8 + for row in range(dest_y, dest_y + dest_h): + offset = row * self.stride + dest_x * bytes_per_pixel + buf[offset:offset + len(row_bytes)] = row_bytes def _text(self, buf, x, y, text, scale, rgb): - for ch in text.upper(): - glyph = FONT.get(ch) + for char in text.upper(): + glyph = FONT.get(char) if glyph: for col in range(GLYPH_W): bits = glyph[col] @@ -275,12 +275,12 @@ class Screen: if len(lines) < max_lines and current: lines.append(current) # Anything still too wide on its own (one very long word) gets cut. - out = [] + clipped = [] for line in lines: while len(line) > 1 and self._width_of(line, scale) > room: line = line[:-1] - out.append(line) - return out + clipped.append(line) + return clipped # -- what callers use -------------------------------------------------- @@ -340,13 +340,13 @@ class Screen: self._center(buf, line, y, detail_scale, colour) y += GLYPH_H * detail_scale + 2 * detail_scale if progress is not None: - bw = int(self.width * 0.56) - bh = max(4, self.height // 100) - bx = (self.width - bw) // 2 - by = int(self.height * 0.58) - self._rect(buf, bx, by, bw, bh, BAR_BG) - filled = int(bw * min(1.0, max(0.0, progress))) - self._rect(buf, bx, by, filled, bh, BAR_FG) + bar_width = int(self.width * 0.56) + bar_height = max(4, self.height // 100) + bar_x = (self.width - bar_width) // 2 + bar_y = int(self.height * 0.58) + self._rect(buf, bar_x, bar_y, bar_width, bar_height, BAR_BG) + filled = int(bar_width * min(1.0, max(0.0, progress))) + self._rect(buf, bar_x, bar_y, filled, bar_height, BAR_FG) if note: self._center(buf, note, int(self.height * 0.78), note_scale, NOTE) @@ -379,18 +379,20 @@ def main(argv): the migration exits at its stamp and draws nothing. """ import argparse - ap = argparse.ArgumentParser(description='draw one frame on /dev/fb0') - ap.add_argument('--fb', default='/dev/fb0') - ap.add_argument('--size', default=None, help='WxH@BPP; default: ask sysfs') - ap.add_argument('--rotate', type=int, default=None, choices=[0, 90, 270]) - ap.add_argument('--title', default='') - ap.add_argument('--status', default='') - ap.add_argument('--detail', default='') - ap.add_argument('--note', default='') - ap.add_argument('--progress', type=float, default=None) - ap.add_argument('--fault', action='store_true') - ap.add_argument('--clear', action='store_true') - args = ap.parse_args(argv) + parser = argparse.ArgumentParser(description='draw one frame on /dev/fb0') + parser.add_argument('--fb', default='/dev/fb0') + parser.add_argument('--size', default=None, + help='WxH@BPP; default: ask sysfs') + parser.add_argument('--rotate', type=int, default=None, + choices=[0, 90, 270]) + parser.add_argument('--title', default='') + parser.add_argument('--status', default='') + parser.add_argument('--detail', default='') + parser.add_argument('--note', default='') + parser.add_argument('--progress', type=float, default=None) + parser.add_argument('--fault', action='store_true') + parser.add_argument('--clear', action='store_true') + args = parser.parse_args(argv) try: screen = Screen(args.fb, geometry=parse_geometry(args.size), rotate=args.rotate) diff --git a/payload/firmwareExe b/payload/firmwareExe index 2d1fc59..e8719bb 100755 --- a/payload/firmwareExe +++ b/payload/firmwareExe @@ -36,8 +36,8 @@ echo "=== mod firmwareExe `date 2>/dev/null` ===" # it -- /usr/prog/start.img exists, but the only two lines that would display # it (both in app_startup.sh) are commented out, so nothing has shown it for # a long time. -FF_PY=/usr/prog/Python-3.8.2/bin/python3 -[ -x "$FF_PY" ] || FF_PY=python3 +FF_PYTHON=/usr/prog/Python-3.8.2/bin/python3 +[ -x "$FF_PYTHON" ] || FF_PYTHON=python3 LD_LIBRARY_PATH=/usr/prog/Python-3.8.2/lib:$LD_LIBRARY_PATH LD_LIBRARY_PATH=/usr/prog/openssl-1.0.2d/lib:$LD_LIBRARY_PATH LD_LIBRARY_PATH=/usr/prog/libffi-3.4.4/lib:$LD_LIBRARY_PATH @@ -45,7 +45,7 @@ export LD_LIBRARY_PATH splash() { [ "${MOD_SPLASH:-1}" = 1 ] || return 0 [ -f $MODDIR/bin/ffscreen.py ] || return 0 - "$FF_PY" $MODDIR/bin/ffscreen.py "$@" 2>/dev/null || true + "$FF_PYTHON" $MODDIR/bin/ffscreen.py "$@" 2>/dev/null || true } splash --title "REFORGE" --status "STARTING THE PRINTER" --progress 0.02 @@ -53,10 +53,10 @@ splash --title "REFORGE" --status "STARTING THE PRINTER" --progress 0.02 # init.d-style scripts, run in filename order. One failing service must never # stop the rest: Klipper has to come up even if the web stack is broken. if [ -d $MODDIR/init.d ]; then - for s in $MODDIR/init.d/S*; do - [ -x "$s" ] || continue - echo "--- `basename $s` ---" - "$s" start || echo "`basename $s`: returned $?" + for service in $MODDIR/init.d/S*; do + [ -x "$service" ] || continue + echo "--- `basename $service` ---" + "$service" start || echo "`basename $service`: returned $?" done fi @@ -78,7 +78,7 @@ if [ "${MOD_STARTUP:-1}" = 1 ] && [ -f $MODDIR/bin/ff-startup.py ]; then STARTUP_ARGS="--timeout ${MOD_STARTUP_TIMEOUT:-240}" STARTUP_ARGS="$STARTUP_ARGS --klipper-tries ${MOD_KLIPPER_TRIES:-6}" [ "${MOD_IMPORT:-1}" = 1 ] || STARTUP_ARGS="$STARTUP_ARGS --no-import" - "$FF_PY" $MODDIR/bin/ff-startup.py $STARTUP_ARGS \ + "$FF_PYTHON" $MODDIR/bin/ff-startup.py $STARTUP_ARGS \ || echo "ff-startup: returned $? -- the next boot will try again" fi diff --git a/payload/init.d/S50wifi b/payload/init.d/S50wifi index 83f6490..4db0e89 100755 --- a/payload/init.d/S50wifi +++ b/payload/init.d/S50wifi @@ -47,11 +47,11 @@ supplicant_running() { connect() { # app_startup.sh insmods 8821cu just before us, but the interface takes a # moment to register. - i=0 - while [ $i -lt 15 ]; do + waited=0 + while [ $waited -lt 15 ]; do [ -d /sys/class/net/$IFACE ] && break sleep 1 - i=$((i + 1)) + waited=$((waited + 1)) done if [ ! -d /sys/class/net/$IFACE ]; then echo "wifi: $IFACE never appeared -- is 8821cu.ko loaded?" @@ -71,11 +71,11 @@ connect() { fi # Give association a chance before asking for a lease. - i=0 - while [ $i -lt 20 ]; do + waited=0 + while [ $waited -lt 20 ]; do /usr/sbin/wpa_cli -i $IFACE status 2>/dev/null | grep -q '^wpa_state=COMPLETED' && break sleep 1 - i=$((i + 1)) + waited=$((waited + 1)) done /sbin/udhcpc -i $IFACE -b -t 5 >/dev/null 2>&1 & echo "wifi: udhcpc requesting a lease on $IFACE" diff --git a/payload/init.d/S60web b/payload/init.d/S60web index b735532..c4d8b2c 100755 --- a/payload/init.d/S60web +++ b/payload/init.d/S60web @@ -12,12 +12,12 @@ MODDIR=/usr/data/anvil [ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf NGINX=/usr/prog/nginx/sbin/nginx -NGX_PID=$MODDIR/nginx/logs/nginx.pid -MR_PID=/run/moonraker.pid -MR_LOG=/usr/data/logs/moonraker.log -MR_TMP=/usr/data/tmp -MR_DATA=/usr/data -MR_PY=/usr/prog/Python-3.8.2/bin/python3 +NGINX_PID=$MODDIR/nginx/logs/nginx.pid +MOONRAKER_PID=/run/moonraker.pid +MOONRAKER_LOG=/usr/data/logs/moonraker.log +MOONRAKER_TMP=/usr/data/tmp +MOONRAKER_DATA=/usr/data +MOONRAKER_PYTHON=/usr/prog/Python-3.8.2/bin/python3 # Which moonraker actually runs. The INSTALLED tree first: run-append.sh # already decided what belongs there -- the mod's build if it passed the @@ -25,8 +25,8 @@ MR_PY=/usr/prog/Python-3.8.2/bin/python3 # that decision here would defeat the rollback it exists to provide. The copy # on the data partition is the fallback for a printer where /usr/prog has no # moonraker at all. -MR_MAIN=/usr/prog/moonraker/moonraker/moonraker/moonraker.py -[ -f "$MR_MAIN" ] || MR_MAIN=$MODDIR/moonraker/moonraker.py +MOONRAKER_MAIN=/usr/prog/moonraker/moonraker/moonraker/moonraker.py +[ -f "$MOONRAKER_MAIN" ] || MOONRAKER_MAIN=$MODDIR/moonraker/moonraker.py start() { [ "${MOD_WEB:-1}" = "1" ] || { echo "web: disabled in anvil.conf"; return 0; } @@ -37,7 +37,7 @@ start() { else echo "nginx: not available" fi - if [ -x "$MR_PY" ]; then + if [ -x "$MOONRAKER_PYTHON" ]; then # This interpreter does not start without its own libraries on the # path: without them python3 dies on "libpython3.8.so.1.0: cannot # open shared object file", moonraker never listens on 7125, and @@ -72,11 +72,13 @@ start() { # * TMPDIR. Moonraker writes temporary files -- uploads among them # -- and /tmp here is a ramdisk, so a large upload without this # fills memory instead of the data partition. - rm -rf $MR_TMP && mkdir -p $MR_TMP - export TMPDIR=$MR_TMP - if [ -f "$MR_MAIN" ]; then - start-stop-daemon -S -b -m -p $MR_PID --exec $MR_PY \ - -- $MR_MAIN -d $MR_DATA && echo "moonraker: started ($MR_MAIN)" + rm -rf $MOONRAKER_TMP && mkdir -p $MOONRAKER_TMP + export TMPDIR=$MOONRAKER_TMP + if [ -f "$MOONRAKER_MAIN" ]; then + start-stop-daemon -S -b -m -p $MOONRAKER_PID \ + --exec $MOONRAKER_PYTHON \ + -- $MOONRAKER_MAIN -d $MOONRAKER_DATA \ + && echo "moonraker: started ($MOONRAKER_MAIN)" moonraker_check & else echo "moonraker: no moonraker.py found -- not started" @@ -89,33 +91,33 @@ start() { # boot log, with the reason, because "moonraker did not start" is otherwise a # support conversation rather than a log line. moonraker_alive() { - [ -f "$MR_PID" ] || return 1 - kill -0 `cat "$MR_PID"` 2>/dev/null + [ -f "$MOONRAKER_PID" ] || return 1 + kill -0 `cat "$MOONRAKER_PID"` 2>/dev/null } moonraker_check() { - n=0 - while [ $n -lt 30 ]; do + waited=0 + while [ $waited -lt 30 ]; do sleep 2 - n=$((n + 1)) + waited=$((waited + 1)) moonraker_alive && continue - echo "moonraker: NOT running after $((n * 2))s -- last words:" - tail -n 15 "$MR_LOG" 2>/dev/null | sed 's/^/ /' + echo "moonraker: NOT running after $((waited * 2))s -- last words:" + tail -n 15 "$MOONRAKER_LOG" 2>/dev/null | sed 's/^/ /' return 1 done echo "moonraker: up" return 0 } stop() { - [ -f "$NGX_PID" ] && kill `cat $NGX_PID` 2>/dev/null - [ -f "$MR_PID" ] && start-stop-daemon -K -p $MR_PID 2>/dev/null + [ -f "$NGINX_PID" ] && kill `cat $NGINX_PID` 2>/dev/null + [ -f "$MOONRAKER_PID" ] && start-stop-daemon -K -p $MOONRAKER_PID 2>/dev/null echo "web: stopped" } case "$1" in start|'') start ;; stop) stop ;; restart) stop; sleep 2; start ;; - status) [ -f "$NGX_PID" ] && echo "nginx: running" || echo "nginx: not running" + status) [ -f "$NGINX_PID" ] && echo "nginx: running" || echo "nginx: not running" moonraker_alive && echo "moonraker: running" || echo "moonraker: not running" ;; *) echo "usage: $0 {start|stop|restart|status}"; exit 1 ;; esac diff --git a/payload/init.d/S65camera b/payload/init.d/S65camera index 5061f1f..796fc10 100755 --- a/payload/init.d/S65camera +++ b/payload/init.d/S65camera @@ -41,10 +41,10 @@ streamer_running() { # 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 + waited=0 + while [ ! -e "$VIDEO" ] && [ $waited -lt 30 ]; do sleep 1 - i=$((i + 1)) + waited=$((waited + 1)) done if [ ! -e "$VIDEO" ]; then echo "camera: $VIDEO never appeared -- no camera plugged in?" diff --git a/payload/init.d/S70klipper b/payload/init.d/S70klipper index fd52228..73ddb4e 100755 --- a/payload/init.d/S70klipper +++ b/payload/init.d/S70klipper @@ -80,8 +80,8 @@ klippy_verdict() { } supervise() { - n=1 - while [ $n -le $KLIPPER_TRIES ]; do + attempt=1 + while [ $attempt -le $KLIPPER_TRIES ]; do # NOT `wc -l < "$PRINTER_LOG" 2>/dev/null`: redirections are applied # left to right, so the input redirect fails and the shell prints # "can't open ...: no such file" BEFORE 2>/dev/null is in effect. On @@ -89,39 +89,42 @@ supervise() { # in the boot log every time, interleaved with whatever else was # writing -- which is what made one user's log unreadable at exactly # the point they were trying to diagnose. - mark=`cat "$PRINTER_LOG" 2>/dev/null | wc -l` - [ -n "$mark" ] || mark=0 + log_lines=`cat "$PRINTER_LOG" 2>/dev/null | wc -l` + [ -n "$log_lines" ] || log_lines=0 # A failed connect takes ~90s to be reported, so wait out more than # that before concluding anything. - w=0 + waited=0 verdict="" - while [ $w -lt 30 ]; do + while [ $waited -lt 30 ]; do sleep 5 - w=$((w + 1)) - verdict=`klippy_verdict "$mark"` + waited=$((waited + 1)) + verdict=`klippy_verdict "$log_lines"` [ -n "$verdict" ] && break done case "$verdict" in ready) - echo "klipper: ready (attempt $n)" + echo "klipper: ready (attempt $attempt)" return 0 ;; "") # Neither marker showed up. Something other than an MCU # connect is going on; restarting would only hide it. - echo "klipper: no verdict after attempt $n -- leaving it alone" + echo "klipper: no verdict after attempt $attempt -- leaving it alone" return 0 ;; esac - echo "klipper: MCU connect failed (attempt $n/$KLIPPER_TRIES) -- restarting" + echo "klipper: MCU connect failed (attempt $attempt/$KLIPPER_TRIES) -- restarting" [ -x /usr/prog/klipper/klipperDaemon ] && /usr/prog/klipper/klipperDaemon stop - g=0 - while klippy_proc && [ $g -lt 15 ]; do sleep 1; g=$((g + 1)); done + stopping=0 + while klippy_proc && [ $stopping -lt 15 ]; do + sleep 1 + stopping=$((stopping + 1)) + done # start.sh bails out if the socket is still there, so clear a stale # one -- but only once klippy is definitely gone. klippy_proc || rm -f /tmp/uds /usr/prog/klipper/start.sh - n=$((n + 1)) + attempt=$((attempt + 1)) done echo "klipper: still no MCU after $KLIPPER_TRIES attempts -- giving up" return 1 diff --git a/payload/klipper/extras/ff_legacy.py b/payload/klipper/extras/ff_legacy.py index 15b9e46..ab6c1d8 100644 --- a/payload/klipper/extras/ff_legacy.py +++ b/payload/klipper/extras/ff_legacy.py @@ -62,65 +62,67 @@ class FFFirmwareConfig: try: with open(path, 'r') as fh: text = fh.read() - except IOError as e: - self.errors.append("%s: %s" % (path, e)) + except IOError as err: + self.errors.append("%s: %s" % (path, err)) continue try: - obj, _ = json.JSONDecoder().raw_decode(text.lstrip()) - except ValueError as e: - self.errors.append("%s: not valid JSON (%s)" % (path, e)) + decoded, _tail = json.JSONDecoder().raw_decode(text.lstrip()) + except ValueError as err: + self.errors.append("%s: not valid JSON (%s)" % (path, err)) continue - if not isinstance(obj, dict): + if not isinstance(decoded, dict): self.errors.append("%s: expected a JSON object" % path) continue - self.files[name] = obj + self.files[name] = decoded - def num(self, fname, key): - obj = self.files.get(fname) - if obj is None: + def num(self, file_name, key): + contents = self.files.get(file_name) + if contents is None: return None - val = obj.get(key) - if isinstance(val, bool) or not isinstance(val, (int, float)): + value = contents.get(key) + if isinstance(value, bool) or not isinstance(value, (int, float)): return None - if not math.isfinite(val): + if not math.isfinite(value): return None - return float(val) + return float(value) def tool_offsets(self): rows = [] for index in range(EXTRUDER_COUNT): - offset = [self.num('extruder', 't%d_offset_%s' % (index, axis)) for axis in 'xyz'] + offset = [self.num('extruder', 't%d_offset_%s' % (index, axis)) + for axis in 'xyz'] rows.append(None if None in offset else tuple(offset)) return rows def docks(self): rows = [] - for n in range(EXTRUDER_COUNT): + for index in range(EXTRUDER_COUNT): # first extruder is just "extruder", next is "extruder1" - sfx = '' if n == 0 else '%d' % n - x = self.num('extruder', 'x_check_pos' + sfx) - y = self.num('extruder', 'y_check_pos' + sfx) - rows.append(None if None in (x, y) else (x, y)) + suffix = '' if index == 0 else '%d' % index + dock_x = self.num('extruder', 'x_check_pos' + suffix) + dock_y = self.num('extruder', 'y_check_pos' + suffix) + rows.append(None if None in (dock_x, dock_y) else (dock_x, dock_y)) return rows def station(self): - v = [self.num('extruder', '%s_station_pos' % a) for a in 'xyz'] - return None if None in v else tuple(v) + position = [self.num('extruder', '%s_station_pos' % axis) + for axis in 'xyz'] + return None if None in position else tuple(position) def z_adjust(self): # zoffset.json is 1-BASED: z_offset_t1 is tool 0. - return [self.num('zoffset', 'z_offset_t%d' % (n + 1)) - for n in range(EXTRUDER_COUNT)] + return [self.num('zoffset', 'z_offset_t%d' % (index + 1)) + for index in range(EXTRUDER_COUNT)] def speed(self, key): # mm/s in the file; the app multiplies by 60 and ignores <= 0. - v = self.num('test', key) - if v is None or v <= 0: + mm_per_second = self.num('test', key) + if mm_per_second is None or mm_per_second <= 0: return None - return int(round(v * 60.0)) + return int(round(mm_per_second * 60.0)) class FFLegacy: @@ -144,19 +146,19 @@ class FFLegacy: directory = gcmd.get('DIR', self.default_dir) apply = gcmd.get_int('APPLY', 1, minval=0, maxval=1) try: - out = self._import(directory, apply) - except ValueError as e: - raise gcmd.error(str(e)) - gcmd.respond_info("\n".join(out)) - logging.info("%s: %s", self.name, "\n".join(out)) + report = self._import(directory, apply) + except ValueError as err: + raise gcmd.error(str(err)) + gcmd.respond_info("\n".join(report)) + logging.info("%s: %s", self.name, "\n".join(report)) def _import(self, directory, apply): - fw = FFFirmwareConfig(directory) - out = ["import from %s" % directory] - out += [" ! %s" % e for e in fw.errors] - if 'extruder' not in fw.files: + firmware = FFFirmwareConfig(directory) + report = ["import from %s" % directory] + report += [" ! %s" % problem for problem in firmware.errors] + if 'extruder' not in firmware.files: raise ValueError("%s: extruder.json could not be read (%s)" - % (self.name, "; ".join(fw.errors))) + % (self.name, "; ".join(firmware.errors))) configfile = self.printer.lookup_object('configfile') staged = [] @@ -167,87 +169,92 @@ class FFLegacy: staged.append(" [%s] %s = %.6f" % (section, option, value)) # -- per-unit data -> autosave ----------------------------------- - tools = fw.tool_offsets() - zadj = fw.z_adjust() - docks = fw.docks() - for n in range(EXTRUDER_COUNT): - sec = 'ff_tool %d' % n - if docks[n] is not None: - stage(sec, 'dock_x', docks[n][0]) - stage(sec, 'dock_y', docks[n][1]) - obj = self.printer.lookup_object(sec, None) - if apply and obj is not None: - obj.dock_x, obj.dock_y = docks[n] + nozzles = firmware.tool_offsets() + z_adjusts = firmware.z_adjust() + docks = firmware.docks() + for index in range(EXTRUDER_COUNT): + section = 'ff_tool %d' % index + if docks[index] is not None: + stage(section, 'dock_x', docks[index][0]) + stage(section, 'dock_y', docks[index][1]) + tool_object = self.printer.lookup_object(section, None) + if apply and tool_object is not None: + tool_object.dock_x, tool_object.dock_y = docks[index] else: - out.append(" ! x/y_check_pos%s missing -- T%d dock not imported" - % ('' if n == 0 else n, n)) - if tools[n] is not None: - stage(sec, 'nozzle_x', tools[n][0]) - stage(sec, 'nozzle_y', tools[n][1]) - stage(sec, 'nozzle_z', tools[n][2]) - obj = self.printer.lookup_object(sec, None) - if apply and obj is not None: - obj.nozzle = tools[n] + report.append(" ! x/y_check_pos%s missing -- T%d dock not" + " imported" + % ('' if index == 0 else index, index)) + if nozzles[index] is not None: + stage(section, 'nozzle_x', nozzles[index][0]) + stage(section, 'nozzle_y', nozzles[index][1]) + stage(section, 'nozzle_z', nozzles[index][2]) + tool_object = self.printer.lookup_object(section, None) + if apply and tool_object is not None: + tool_object.nozzle = nozzles[index] else: - out.append(" ! t%d_offset_x/y/z missing -- T%d not imported" - % (n, n)) - if zadj[n]: - stage(sec, 'z_adjust', zadj[n]) - obj = self.printer.lookup_object(sec, None) - if apply and obj is not None: - obj.z_adjust = zadj[n] - st = fw.station() - if st is not None: - stage('ff_tool_offset', 'station_x', st[0]) - stage('ff_tool_offset', 'station_y', st[1]) - stage('ff_tool_offset', 'station_z', st[2]) - obj = self.printer.lookup_object('ff_tool_offset', None) - if apply and obj is not None: - obj.station = st + report.append(" ! t%d_offset_x/y/z missing -- T%d not" + " imported" % (index, index)) + if z_adjusts[index]: + stage(section, 'z_adjust', z_adjusts[index]) + tool_object = self.printer.lookup_object(section, None) + if apply and tool_object is not None: + tool_object.z_adjust = z_adjusts[index] + station = firmware.station() + if station is not None: + stage('ff_tool_offset', 'station_x', station[0]) + stage('ff_tool_offset', 'station_y', station[1]) + stage('ff_tool_offset', 'station_z', station[2]) + offsets = self.printer.lookup_object('ff_tool_offset', None) + if apply and offsets is not None: + offsets.station = station else: - out.append(" ! x/y/z_station_pos missing -- station not imported") - obj = self.printer.lookup_object('ff_tool_offset', None) - for opt, attr in (('cylinder_x', 'cfg_cylinder_x'), - ('cylinder_y', 'cfg_cylinder_y')): - v = fw.num('test', opt) - if v is not None: - stage('ff_tool_offset', opt, v) - if apply and obj is not None: - setattr(obj, attr, v) + report.append(" ! x/y/z_station_pos missing -- station not" + " imported") + offsets = self.printer.lookup_object('ff_tool_offset', None) + for option, attribute in (('cylinder_x', 'cfg_cylinder_x'), + ('cylinder_y', 'cfg_cylinder_y')): + value = firmware.num('test', option) + if value is not None: + stage('ff_tool_offset', option, value) + if apply and offsets is not None: + setattr(offsets, attribute, value) - tc = self.printer.lookup_object('ff_toolchange', None) - if apply and tc is not None and hasattr(tc, 'refresh_offsets'): - tc.refresh_offsets() + toolchange = self.printer.lookup_object('ff_toolchange', None) + if apply and toolchange is not None \ + and hasattr(toolchange, 'refresh_offsets'): + toolchange.refresh_offsets() - out.append("%s for SAVE_CONFIG:" % ("staged" if apply else "would stage")) - out += staged or [" (nothing)"] + report.append("%s for SAVE_CONFIG:" + % ("staged" if apply else "would stage")) + report += staged or [" (nothing)"] # -- plain [ff_toolchange] settings -> snippet, only if different -- wanted = [] - for opt, key in (('x_correction', 'grabOffset'), - ('temp_offset', 'tempOffset')): - v = fw.num('test', key) - if v is not None: - wanted.append((opt, v, "%.6g")) - fast = fw.speed('grabSpeed') - slow = fw.speed('grabSpeedSlow') - if fast is not None: - wanted.append(('fast_feed', fast, "%d")) - if slow is not None: - wanted.append(('slow_feed', slow, "%d")) - wanted.append(('release_slow_feed', slow, "%d")) + for option, key in (('x_correction', 'grabOffset'), + ('temp_offset', 'tempOffset')): + value = firmware.num('test', key) + if value is not None: + wanted.append((option, value, "%.6g")) + fast_feed = firmware.speed('grabSpeed') + slow_feed = firmware.speed('grabSpeedSlow') + if fast_feed is not None: + wanted.append(('fast_feed', fast_feed, "%d")) + if slow_feed is not None: + wanted.append(('slow_feed', slow_feed, "%d")) + wanted.append(('release_slow_feed', slow_feed, "%d")) snippet = [] - for opt, v, fmt in wanted: - cur = getattr(tc, opt, None) if tc is not None else None - if cur is None or abs(float(cur) - float(v)) > 1e-9: - snippet.append(("%s: " + fmt) % (opt, v)) + for option, value, fmt in wanted: + running = (getattr(toolchange, option, None) + if toolchange is not None else None) + if running is None or abs(float(running) - float(value)) > 1e-9: + snippet.append(("%s: " + fmt) % (option, value)) if snippet: - out.append("[ff_toolchange] differs from the factory JSON --" - " set by hand in ff-toolchange.cfg if intended:") - out += [" " + l for l in snippet] + report.append("[ff_toolchange] differs from the factory JSON --" + " set by hand in ff-toolchange.cfg if intended:") + report += [" " + line for line in snippet] if apply and staged: - out.append("Then run SAVE_CONFIG to persist the staged values.") - return out + report.append("Then run SAVE_CONFIG to persist the staged values.") + return report def load_config(config): diff --git a/payload/klipper/extras/ff_print.py b/payload/klipper/extras/ff_print.py index 5b2dfce..85dd3da 100644 --- a/payload/klipper/extras/ff_print.py +++ b/payload/klipper/extras/ff_print.py @@ -88,42 +88,43 @@ def _parse_metadata(path): not present. Never raises -- a file we cannot read just yields {}, and the macro then runs with no derived parameters.""" try: - with open(path, 'rb') as f: - head = f.read(HEAD_BYTES).decode('utf-8', 'replace') + with open(path, 'rb') as fh: + head = fh.read(HEAD_BYTES).decode('utf-8', 'replace') except Exception: logging.exception("ff_print: cannot read '%s'", path) return {} - meta = {} + metadata = {} # Bed and nozzle: the file's own first heat command, the way the app's # parser did it (it scanned M104/M109/M140/M190/M141/M191 and reported # nozzleTemp / bedTemp / chamberTemp). - bed = re.search(r'^M1[49]0 S([0-9]+(?:\.[0-9]+)?)', head, re.M) - if bed is not None and float(bed.group(1)) > 0: - meta['bed'] = int(float(bed.group(1))) - hot = re.search(r'^M10[49] S([0-9]+(?:\.[0-9]+)?)', head, re.M) - if hot is not None and float(hot.group(1)) > 0: - meta['nozzle'] = int(float(hot.group(1))) + bed_match = re.search(r'^M1[49]0 S([0-9]+(?:\.[0-9]+)?)', head, re.M) + if bed_match is not None and float(bed_match.group(1)) > 0: + metadata['bed'] = int(float(bed_match.group(1))) + nozzle_match = re.search(r'^M10[49] S([0-9]+(?:\.[0-9]+)?)', head, re.M) + if nozzle_match is not None and float(nozzle_match.group(1)) > 0: + metadata['nozzle'] = int(float(nozzle_match.group(1))) # The file's initial extruder (the app's "fisrNozzleIndex", sic). - first = re.search(r'^T([0-%d])\b' % (EXTRUDER_COUNT - 1), head, re.M) - if first is not None: - meta['tool'] = int(first.group(1)) + tool_match = re.search(r'^T([0-%d])\b' % (EXTRUDER_COUNT - 1), + head, re.M) + if tool_match is not None: + metadata['tool'] = int(tool_match.group(1)) # First-layer height, from the per-layer marker the slicer emits in the # body -- one of the three keys the app looked for, and the only one that # appears near the start of the file. This feeds the print Z offset's # thin-layer term, which is a FIRST-layer correction, so the first layer's # height is the value that belongs there. - layer = re.search(r'^;HEIGHT:([0-9.]+)', head, re.M) - if layer is not None: + layer_match = re.search(r'^;HEIGHT:([0-9.]+)', head, re.M) + if layer_match is not None: try: - meta['layer'] = float(layer.group(1)) + metadata['layer'] = float(layer_match.group(1)) except ValueError: pass - return meta + return metadata class FFPrint: @@ -137,8 +138,8 @@ class FFPrint: in config.getlist('hook_commands', ['SDCARD_PRINT_FILE', 'M23']) if c.strip()] - self.prev_handlers = {} - self.meta = {} + self.previous_handlers = {} + self.metadata = {} self.filename = None self.origin = None # Latch: only a print WE announced may fire the end macro. @@ -154,14 +155,14 @@ class FFPrint: # Same rename dance gcode_macro uses for rename_existing: take the # command over and keep the previous handler to chain to. for cmd in self.hook_commands: - prev = self.gcode.register_command(cmd, None) - if prev is None: + previous = self.gcode.register_command(cmd, None) + if previous is None: raise self.printer.config_error( "ff_print: command '%s' is not registered -- is there a" " [virtual_sdcard] section in the config? (Section ORDER" " does not matter: this runs at klippy:connect, after" " every section is loaded.)" % (cmd,)) - self.prev_handlers[cmd] = prev + self.previous_handlers[cmd] = previous self.gcode.register_command( cmd, self._make_handler(cmd), desc="%s (ff_print: runs %s first)" @@ -178,10 +179,10 @@ class FFPrint: 'filename': self.filename, 'origin': self.origin, 'active': self.active, - 'tool': self.meta.get('tool'), - 'nozzle': self.meta.get('nozzle'), - 'bed': self.meta.get('bed'), - 'layer': self.meta.get('layer'), + 'tool': self.metadata.get('tool'), + 'nozzle': self.metadata.get('nozzle'), + 'bed': self.metadata.get('bed'), + 'layer': self.metadata.get('layer'), } def _resolve(self, gcmd, cmd): @@ -196,18 +197,18 @@ class FFPrint: name = name[1:] if not name: return None - vsd = self.printer.lookup_object('virtual_sdcard', None) - if vsd is None: + virtual_sdcard = self.printer.lookup_object('virtual_sdcard', None) + if virtual_sdcard is None: return None - return os.path.join(vsd.sdcard_dirname, name) + return os.path.join(virtual_sdcard.sdcard_dirname, name) def _cmd_start(self, cmd, gcmd): path = self._resolve(gcmd, cmd) self.filename = path self.origin = cmd - self.meta = _parse_metadata(path) if path else {} - if self.meta: - logging.info("ff_print: %s -> %s", path, self.meta) + self.metadata = _parse_metadata(path) if path else {} + if self.metadata: + logging.info("ff_print: %s -> %s", path, self.metadata) else: logging.info("ff_print: %s -> no slicer metadata found", path) @@ -215,8 +216,8 @@ class FFPrint: for key, name, fmt in (('bed', 'BED', '%d'), ('tool', 'TOOL', '%d'), ('nozzle', 'NOZZLE', '%d'), ('layer', 'LAYER', '%s')): - if self.meta.get(key) is not None: - params.append('%s=%s' % (name, fmt % (self.meta[key],))) + if self.metadata.get(key) is not None: + params.append('%s=%s' % (name, fmt % (self.metadata[key],))) # Let the macro raise: a refusal here must stop the print BEFORE the # base command loads and resumes the file. @@ -224,7 +225,7 @@ class FFPrint: '%s %s' % (self.before_macro, ' '.join(params))) # Arm the end latch only once prepare succeeded. self.active = True - self.prev_handlers[cmd](gcmd) + self.previous_handlers[cmd](gcmd) def _handle_ready(self, print_time): """idle_timeout:ready fires whenever the queue drains -- including a diff --git a/payload/klipper/extras/ff_tool.py b/payload/klipper/extras/ff_tool.py index 4f1a306..c589d13 100644 --- a/payload/klipper/extras/ff_tool.py +++ b/payload/klipper/extras/ff_tool.py @@ -57,7 +57,8 @@ class FFTool: self.extruder_name = ('extruder' if self.index == 0 else 'extruder%d' % self.index) # Measured (TOOL_OFFSET_CALIBRATE) -- one probe run yields all three. - nozzle = [config.getfloat('nozzle_' + a, None) for a in 'xyz'] + nozzle = [config.getfloat('nozzle_' + axis, None) + for axis in 'xyz'] if None in nozzle and nozzle != [None, None, None]: raise config.error("%s: nozzle_x, nozzle_y and nozzle_z must be" " set together" % self.name) @@ -101,11 +102,14 @@ class FFTool: configfile.set(self.name, 'z_adjust', "%.6f" % z) def get_status(self, eventtime): - nx, ny, nz = self.nozzle if self.nozzle else (None, None, None) + nozzle_x, nozzle_y, nozzle_z = (self.nozzle if self.nozzle + else (None, None, None)) return {'index': self.index, 'dock_x': self.dock_x, 'dock_y': self.dock_y, 'extruder': self.extruder_name, - 'z_adjust': self.z_adjust, 'calibrated': self.nozzle is not None, - 'nozzle_x': nx, 'nozzle_y': ny, 'nozzle_z': nz} + 'z_adjust': self.z_adjust, + 'calibrated': self.nozzle is not None, + 'nozzle_x': nozzle_x, 'nozzle_y': nozzle_y, + 'nozzle_z': nozzle_z} def load_config_prefix(config): diff --git a/payload/klipper/extras/ff_tool_offset.py b/payload/klipper/extras/ff_tool_offset.py index df049c5..f954e4f 100644 --- a/payload/klipper/extras/ff_tool_offset.py +++ b/payload/klipper/extras/ff_tool_offset.py @@ -74,56 +74,64 @@ CYLINDER_Y_DEFAULT = 214.5 # Port of BaseFunction::fitCircleStable @0x6467f8 (centroid-shifted algebraic # least squares, cv::solve DECOMP_LU on the 3x3 normal equations): # A = [2x', 2y', 1], b = x'^2 + y'^2, (A^T A) p = A^T b -# centre = (p0 + xbar, p1 + ybar), r = sqrt(p2 + p0^2 + p1^2) +# centre = (p0 + centroid_x, p1 + centroid_y), r = sqrt(p2 + p0^2 + p1^2) # The shipped variant calls fitCircleByLeastSquares @0x645b10, the same # algebraic fit without the centroid shift; for the 4 symmetric points the -# sequence produces both reduce to cx = (px1+px3)/2, cy = (py2+py4)/2. We +# sequence produces both reduce to centre_x = (x_plus+x_minus)/2, +# centre_y = (y_plus+y_minus)/2. We # keep the shifted form for numerical hygiene and accept >= 3 points. # Pure Python on purpose: numpy is not guaranteed on the X2000 rootfs. # --------------------------------------------------------------------------- def fit_circle(points): - """Return (cx, cy, r, residuals); raise ValueError if unfittable.""" - n = len(points) - if n < 3: - raise ValueError("circle fit needs at least 3 points, got %d" % n) - xbar = sum(p[0] for p in points) / n - ybar = sum(p[1] for p in points) / n - m = [[0.0] * 3 for _ in range(3)] - v = [0.0] * 3 + """Return (center_x, center_y, radius, residuals); raise ValueError if + unfittable.""" + point_count = len(points) + if point_count < 3: + raise ValueError("circle fit needs at least 3 points, got %d" + % point_count) + centroid_x = sum(point[0] for point in points) / point_count + centroid_y = sum(point[1] for point in points) / point_count + normal_matrix = [[0.0] * 3 for _ in range(3)] + normal_rhs = [0.0] * 3 for x, y in points: - xs, ys = x - xbar, y - ybar - row = (2.0 * xs, 2.0 * ys, 1.0) - b = xs * xs + ys * ys + shifted_x, shifted_y = x - centroid_x, y - centroid_y + design_row = (2.0 * shifted_x, 2.0 * shifted_y, 1.0) + squared_radius_term = shifted_x * shifted_x + shifted_y * shifted_y for i in range(3): - v[i] += row[i] * b + normal_rhs[i] += design_row[i] * squared_radius_term for j in range(3): - m[i][j] += row[i] * row[j] - p = _solve3(m, v) - if p is None: + normal_matrix[i][j] += design_row[i] * design_row[j] + solution = _solve3(normal_matrix, normal_rhs) + if solution is None: raise ValueError("circle fit is singular (points collinear?)") - cx, cy = p[0] + xbar, p[1] + ybar - rr = p[2] + p[0] * p[0] + p[1] * p[1] - if rr <= 0.0: - raise ValueError("circle fit gave radius^2 = %.6f" % rr) - r = math.sqrt(rr) - resid = [math.hypot(x - cx, y - cy) - r for x, y in points] - return cx, cy, r, resid + center_x = solution[0] + centroid_x + center_y = solution[1] + centroid_y + radius_squared = (solution[2] + solution[0] * solution[0] + + solution[1] * solution[1]) + if radius_squared <= 0.0: + raise ValueError("circle fit gave radius^2 = %.6f" % radius_squared) + radius = math.sqrt(radius_squared) + residuals = [math.hypot(x - center_x, y - center_y) - radius + for x, y in points] + return center_x, center_y, radius, residuals -def _solve3(m, v): - a = [m[i][:] + [v[i]] for i in range(3)] - for c in range(3): - piv = max(range(c, 3), key=lambda r: abs(a[r][c])) - if abs(a[piv][c]) < 1e-12: +def _solve3(matrix, rhs): + """Gauss-Jordan with partial pivoting on the augmented 3x4 system.""" + augmented = [matrix[i][:] + [rhs[i]] for i in range(3)] + for col in range(3): + pivot_row = max(range(col, 3), key=lambda r: abs(augmented[r][col])) + if abs(augmented[pivot_row][col]) < 1e-12: return None - a[c], a[piv] = a[piv], a[c] - for r in range(3): - if r != c: - f = a[r][c] / a[c][c] - for k in range(c, 4): - a[r][k] -= f * a[c][k] - return [a[i][3] / a[i][i] for i in range(3)] + augmented[col], augmented[pivot_row] = (augmented[pivot_row], + augmented[col]) + for row in range(3): + if row != col: + factor = augmented[row][col] / augmented[col][col] + for term in range(col, 4): + augmented[row][term] -= factor * augmented[col][term] + return [augmented[i][3] / augmented[i][i] for i in range(3)] class FFToolOffsetError(Exception): @@ -187,13 +195,14 @@ class FFToolOffset: # Station position measured with the empty carriage # (STATION_CALIBRATE), autosaved here as station_x/y/z. - sx = config.getfloat('station_x', None) - sy = config.getfloat('station_y', None) - sz = config.getfloat('station_z', None) - if (sx, sy, sz) != (None, None, None) and None in (sx, sy, sz): + station_x = config.getfloat('station_x', None) + station_y = config.getfloat('station_y', None) + station_z = config.getfloat('station_z', None) + station = (station_x, station_y, station_z) + if station != (None, None, None) and None in station: raise config.error("%s: station_x, station_y and station_z must" " be set together" % self.name) - self.station = None if sx is None else (sx, sy, sz) + self.station = None if station_x is None else station self.estop = {} self.tools = [] @@ -219,10 +228,10 @@ class FFToolOffset: def _handle_connect(self): missing = [] for axis in 'XYZ': - obj = self.printer.lookup_object('e_stop %s' % axis, None) - if obj is None or not hasattr(obj, 'run_probe'): + axis_probe = self.printer.lookup_object('e_stop %s' % axis, None) + if axis_probe is None or not hasattr(axis_probe, 'run_probe'): missing.append('e_stop %s' % axis) - self.estop[axis] = obj + self.estop[axis] = axis_probe if missing: raise self.printer.config_error( "%s: required sections not found: %s (the station sensor is" @@ -245,15 +254,15 @@ class FFToolOffset: return x, y def _check_homed(self, gcmd): - th = self.printer.lookup_object('toolhead') - homed = th.get_status(self.reactor.monotonic())['homed_axes'] - if not all(a in homed for a in 'xyz'): + toolhead = self.printer.lookup_object('toolhead') + homed = toolhead.get_status(self.reactor.monotonic())['homed_axes'] + if not all(axis in homed for axis in 'xyz'): raise gcmd.error("%s: home all axes first (homed: '%s')" % (self.name, homed)) def _current_accel(self): - th = self.printer.lookup_object('toolhead') - return th.get_status(self.reactor.monotonic())['max_accel'] + toolhead = self.printer.lookup_object('toolhead') + return toolhead.get_status(self.reactor.monotonic())['max_accel'] def _estop(self, gcmd, axis, target): """One ESTOP AXES= TARGET=, calling the fork's e_stop @@ -261,16 +270,16 @@ class FFToolOffset: the command: run_probe() takes sub_cycle_cnt samples (3 in printer.base.cfg) each retracting back_v, rejects a spread above error_v, and raises on no-trigger / triggered-before-move.""" - obj = self.estop[axis] - obj.position_offset = target + axis_probe = self.estop[axis] + axis_probe.position_offset = target try: - pos = obj.run_probe(gcmd) - except self.printer.command_error as e: + triggered_at = axis_probe.run_probe(gcmd) + except self.printer.command_error as err: raise FFToolOffsetError("ESTOP %s TARGET=%.3f failed: %s" - % (axis, target, e)) - if pos is None: + % (axis, target, err)) + if triggered_at is None: raise FFToolOffsetError("ESTOP %s returned no position" % axis) - return float(pos) + return float(triggered_at) # ---------------- the recovered sequence ---------------- @@ -288,34 +297,38 @@ class FFToolOffset: self._run('G1 X%.3f Y%.3f F%d' % (x0, y0, return_feed)) self._run('M400') - d = self.probe_travel - px1 = self._estop(gcmd, 'X', x0 + d) + reach = self.probe_travel + x_plus = self._estop(gcmd, 'X', x0 + reach) back() - py2 = self._estop(gcmd, 'Y', y0 + d) + y_plus = self._estop(gcmd, 'Y', y0 + reach) back() - px3 = self._estop(gcmd, 'X', x0 - d) + x_minus = self._estop(gcmd, 'X', x0 - reach) back() - py4 = self._estop(gcmd, 'Y', y0 - d) + y_minus = self._estop(gcmd, 'Y', y0 - reach) back() - pts = [(px1, y0), (x0, py2), (px3, y0), (x0, py4)] - for i, p in enumerate(pts): - gcmd.respond_info(" Point%d: %.4f, %.4f" % (i + 1, p[0], p[1])) + points = [(x_plus, y0), (x0, y_plus), (x_minus, y0), (x0, y_minus)] + for index, point in enumerate(points): + gcmd.respond_info(" Point%d: %.4f, %.4f" + % (index + 1, point[0], point[1])) try: - cx, cy, r, resid = fit_circle(pts) - except ValueError as e: - raise FFToolOffsetError("circle fit failed: %s" % e) - worst = max(abs(e) for e in resid) + center_x, center_y, radius, residuals = fit_circle(points) + except ValueError as err: + raise FFToolOffsetError("circle fit failed: %s" % err) + worst_residual = max(abs(residual) for residual in residuals) gcmd.respond_info(" centre %.4f, %.4f radius %.4f max residual %.4f" - % (cx, cy, r, worst)) - if self.max_residual and worst > self.max_residual: + % (center_x, center_y, radius, worst_residual)) + if self.max_residual and worst_residual > self.max_residual: raise FFToolOffsetError( "fit residual %.4f exceeds max_residual %.4f -- a probe" - " mis-triggered; nothing saved" % (worst, self.max_residual)) - if self.min_radius and r < self.min_radius: - raise FFToolOffsetError("fitted radius %.4f below min_radius" % r) - if self.max_radius and r > self.max_radius: - raise FFToolOffsetError("fitted radius %.4f above max_radius" % r) - return cx, cy, r + " mis-triggered; nothing saved" + % (worst_residual, self.max_residual)) + if self.min_radius and radius < self.min_radius: + raise FFToolOffsetError("fitted radius %.4f below min_radius" + % radius) + if self.max_radius and radius > self.max_radius: + raise FFToolOffsetError("fitted radius %.4f above max_radius" + % radius) + return center_x, center_y, radius def _z_target_for(self, nominal, expected): """Stop z_margin below a known trigger height instead of going all @@ -339,42 +352,46 @@ class FFToolOffset: sideways probe finds no edge. Raises FFToolOffsetError either way, so nothing is damaged and nothing is saved. Runs inside _with_accel_guard.""" - cyl_x, cyl_y = self._cylinder() - expected = self.station[2] if self.station is not None else None - z_t = self._z_target_for(self.z_target, expected) + cylinder_x, cylinder_y = self._cylinder() + expected_z = self.station[2] if self.station is not None else None + z_target = self._z_target_for(self.z_target, expected_z) self._run('SET_GCODE_OFFSET X=0 Y=0 Z=0 MOVE=0 MOVE_SPEED=600') self._run('M400') self._run('G1 Z%.3f F%d' % (self.z_start, FEED_PASS1)) - self._run('G1 X%.3f Y%.3f F%d' % (cyl_x, cyl_y, FEED_POSITION)) + self._run('G1 X%.3f Y%.3f F%d' + % (cylinder_x, cylinder_y, FEED_POSITION)) self._run('SET_VELOCITY_LIMIT ACCEL=%.0f' % self.probe_accel) self._run('M400') hint = (" -- is the build plate still on? Nothing has moved with" " a nozzle. Remove the plate, or plate_check: False in" " [ff_tool_offset] if you are sure.") try: - zp = self._estop(gcmd, 'Z', z_t) - except FFToolOffsetError as e: + station_z = self._estop(gcmd, 'Z', z_target) + except FFToolOffsetError as err: raise FFToolOffsetError("plate check: station Z probe failed" - " (%s)%s" % (e, hint)) - gcmd.respond_info(" plate check: station Z %.3f" % zp) - if expected is not None and zp > expected + self.plate_z_tolerance: + " (%s)%s" % (err, hint)) + gcmd.respond_info(" plate check: station Z %.3f" % station_z) + if expected_z is not None \ + and station_z > expected_z + self.plate_z_tolerance: raise FFToolOffsetError( "plate check: station Z %.3f is %.2f mm above the" - " calibrated %.3f%s" % (zp, zp - expected, expected, hint)) - self._run('G1 X%.3f Y%.3f F%d' % (cyl_x, cyl_y, FEED_PASS1)) - self._run('G1 Z%.3f F%d' % (zp + self.z_clear, FEED_PASS1)) + " calibrated %.3f%s" + % (station_z, station_z - expected_z, expected_z, hint)) + self._run('G1 X%.3f Y%.3f F%d' % (cylinder_x, cylinder_y, FEED_PASS1)) + self._run('G1 Z%.3f F%d' % (z_trigger + self.z_clear, FEED_PASS1)) self._run('M400') try: - px = self._estop(gcmd, 'X', cyl_x + self.probe_travel) - except FFToolOffsetError as e: + edge_x = self._estop(gcmd, 'X', cylinder_x + self.probe_travel) + except FFToolOffsetError as err: raise FFToolOffsetError( "plate check: no circle edge within %.0f mm of the start" - " point (%s)%s" % (self.probe_travel, e, hint)) - self._run('G1 X%.3f F%d' % (cyl_x, FEED_PASS1)) + " point (%s)%s" % (self.probe_travel, err, hint)) + self._run('G1 X%.3f F%d' % (cylinder_x, FEED_PASS1)) self._run('G1 Z%.3f F%d' % (self.z_start, FEED_PASS1)) self._run('M400') gcmd.respond_info(" plate check: circle edge at X %.3f (%+.2f from" - " the start point) -- plate is off" % (px, px - cyl_x)) + " the start point) -- plate is off" + % (edge_x, edge_x - cylinder_x)) def _run_plate_check(self, gcmd): """Park whatever is mounted, then _plate_check. Shared prologue of @@ -383,25 +400,29 @@ class FFToolOffset: minval=0, maxval=1): return if self.toolchange is not None: - cur = self.toolchange.get_status(self.reactor.monotonic()) - if cur.get('current_tool', -1) != -1: + carriage = self.toolchange.get_status(self.reactor.monotonic()) + if carriage.get('current_tool', -1) != -1: self._run('TOOLCHANGE_PARK') self._wait_moves() - cur = self.toolchange.get_status(self.reactor.monotonic()) - if cur.get('current_tool', -1) != -1 or not cur.get('state_ok'): + carriage = self.toolchange.get_status(self.reactor.monotonic()) + if carriage.get('current_tool', -1) != -1 \ + or not carriage.get('state_ok'): raise gcmd.error( "%s: carriage is not verifiably empty (%s) -- cannot run" - " the plate check" % (self.name, cur.get('state_reason'))) + " the plate check" + % (self.name, carriage.get('state_reason'))) self._with_accel_guard(gcmd, lambda: self._plate_check(gcmd)) def _two_pass(self, gcmd, x0, y0, z_target_2, expected_z=None): - """moveCylinderPos + both passes. Returns (cx2, cy2, zP2). + """moveCylinderPos + both passes. + + Returns (center_x_pass2, center_y_pass2, z_trigger_pass2). Caller has already put the right thing on the carriage (tool or nothing) and ensured we are homed. expected_z, when known, bounds both Z probes to expected_z - z_margin.""" - z_t1 = self._z_target_for(self.z_target, expected_z) - z_t2 = self._z_target_for(z_target_2, expected_z) + z_target_pass1 = self._z_target_for(self.z_target, expected_z) + z_target_pass2 = self._z_target_for(z_target_2, expected_z) # moveCylinderPos: zero the G-code offset (the grab applied this # tool's SET_GCODE_OFFSET), lift, travel, Z probe. self._run('SET_GCODE_OFFSET X=0 Y=0 Z=0 MOVE=0 MOVE_SPEED=600') @@ -411,38 +432,43 @@ class FFToolOffset: self._run('SET_VELOCITY_LIMIT ACCEL=%.0f' % self.probe_accel) self._run('M400') self._run('G1 Z%.3f F%d' % (self.z_start, FEED_PASS1)) - gcmd.respond_info(" Z probe to %.3f" % z_t1) - zp = self._estop(gcmd, 'Z', z_t1) - gcmd.respond_info(" Z probe pos: %.4f" % zp) + gcmd.respond_info(" Z probe to %.3f" % z_target_pass1) + z_trigger = self._estop(gcmd, 'Z', z_target_pass1) + gcmd.respond_info(" Z probe pos: %.4f" % z_trigger) # pass 1 self._run('G1 X%.3f Y%.3f F%d' % (x0, y0, FEED_PASS1)) - self._run('G1 Z%.3f F%d' % (zp + self.z_clear, FEED_PASS1)) + self._run('G1 Z%.3f F%d' % (z_trigger + self.z_clear, FEED_PASS1)) self._run('M400') gcmd.respond_info(" pass 1 around %.3f, %.3f at Z %.3f" - % (x0, y0, zp + self.z_clear)) - cx, cy, _r = self._four_points(gcmd, x0, y0, FEED_PASS1, False) + % (x0, y0, z_trigger + self.z_clear)) + center_x, center_y, _radius = self._four_points( + gcmd, x0, y0, FEED_PASS1, False) # G-code is formatted to 3 decimals (the app's float_to_string(v, 3)), # so the centre we actually return to is the rounded one; use that # for the pass-2 point coordinates too. - cx, cy = round(cx, 3), round(cy, 3) + center_x, center_y = round(center_x, 3), round(center_y, 3) # pass 2: centred on the fit, Z re-probed there - self._run('G1 Z%.3f F%d' % (zp + self.z_lift, FEED_PASS1)) - self._run('G1 Y%.3f F%d' % (cy, FEED_PASS2)) - self._run('G1 X%.3f F%d' % (cx, FEED_PASS2)) + self._run('G1 Z%.3f F%d' % (z_trigger + self.z_lift, FEED_PASS1)) + self._run('G1 Y%.3f F%d' % (center_y, FEED_PASS2)) + self._run('G1 X%.3f F%d' % (center_x, FEED_PASS2)) self._run('M400') - zp2 = self._estop(gcmd, 'Z', self._z_target_for(z_t2, zp)) - gcmd.respond_info(" double Z probe pos: %.4f" % zp2) - self._run('G1 Z%.3f F%d' % (zp2 + self.z_clear, FEED_PASS1)) + z_trigger_pass2 = self._estop( + gcmd, 'Z', self._z_target_for(z_target_pass2, z_trigger)) + gcmd.respond_info(" double Z probe pos: %.4f" % z_trigger_pass2) + self._run('G1 Z%.3f F%d' + % (z_trigger_pass2 + self.z_clear, FEED_PASS1)) self._run('M400') gcmd.respond_info(" pass 2 around %.3f, %.3f at Z %.3f" - % (cx, cy, zp2 + self.z_clear)) - cx2, cy2, _r2 = self._four_points(gcmd, cx, cy, FEED_PASS2, True) + % (center_x, center_y, + z_trigger_pass2 + self.z_clear)) + center_x_pass2, center_y_pass2, _radius_pass2 = self._four_points( + gcmd, center_x, center_y, FEED_PASS2, True) self._run('G1 Z%.3f F%d' % (self.z_final, FEED_PASS1)) self._run('M400') - return cx2, cy2, zp2 + return center_x_pass2, center_y_pass2, z_trigger_pass2 def _with_accel_guard(self, gcmd, body): """Run body() with the app's exit block guaranteed: lift, restore @@ -452,13 +478,13 @@ class FFToolOffset: restore = self._current_accel() try: return body() - except FFToolOffsetError as e: + except FFToolOffsetError as err: try: self._run('G1 Z%.3f F%d' % (self.z_start, FEED_PASS1)) self._run('M400') except self.printer.command_error: pass - raise gcmd.error("%s: %s" % (self.name, e)) + raise gcmd.error("%s: %s" % (self.name, err)) finally: try: self._run('SET_VELOCITY_LIMIT ACCEL=%.0f' % restore) @@ -493,19 +519,19 @@ class FFToolOffset: " [PLATE_CHECK=1])") def cmd_TOOL_OFFSET_CALIBRATE(self, gcmd): - raw = gcmd.get('TOOL') - if raw.upper() == 'ALL': + requested = gcmd.get('TOOL') + if requested.upper() == 'ALL': tools = list(range(EXTRUDER_COUNT)) else: try: - t = int(raw) + tool_index = int(requested) except ValueError: raise gcmd.error("TOOL must be 0..%d or ALL" % (EXTRUDER_COUNT - 1)) - if not 0 <= t < EXTRUDER_COUNT: + if not 0 <= tool_index < EXTRUDER_COUNT: raise gcmd.error("TOOL must be 0..%d or ALL" % (EXTRUDER_COUNT - 1)) - tools = [t] + tools = [tool_index] grab = gcmd.get_int('GRAB', 1, minval=0, maxval=1) release = gcmd.get_int('RELEASE', 0, minval=0, maxval=1) save = gcmd.get_int('SAVE', 1, minval=0, maxval=1) @@ -534,8 +560,8 @@ class FFToolOffset: " PLATE_REMOVED=1), or use GRAB=1 and let the toolchanger" " pick it up." % (self.name, tools[0] if tools else '?')) self._run_plate_check(gcmd) - cyl_x, cyl_y = self._cylinder() - x0, y0 = cyl_x - self.nozzle_x_shift, cyl_y + cylinder_x, cylinder_y = self._cylinder() + x0, y0 = cylinder_x - self.nozzle_x_shift, cylinder_y results = {} for tool in tools: @@ -548,8 +574,9 @@ class FFToolOffset: self._run('T%d' % tool) self._wait_moves() - t = self.tools[tool] - expected_z = t.nozzle[2] if t.calibrated() else None + tool_object = self.tools[tool] + expected_z = (tool_object.nozzle[2] + if tool_object.calibrated() else None) if expected_z is None and self.station is not None: # nozzle fires ~3.2 mm above the empty-carriage trigger expected_z = self.station[2] + 0.5 * (self.gap_min @@ -558,20 +585,20 @@ class FFToolOffset: def body(): return self._two_pass(gcmd, x0, y0, self.z_target, expected_z) - cx, cy, zp = self._with_accel_guard(gcmd, body) + center_x, center_y, z_trigger = self._with_accel_guard(gcmd, body) if self.station is not None and (self.gap_min or self.gap_max): - gap = zp - self.station[2] + gap = z_trigger - self.station[2] if gap < self.gap_min or gap > self.gap_max: raise gcmd.error( "%s: T%d nozzle_z %.3f is %.3f above station_z %.3f," " outside gap_min/gap_max [%.2f, %.2f] -- probe" " mis-trigger suspected, nothing saved" - % (self.name, tool, zp, gap, self.station[2], + % (self.name, tool, z_trigger, gap, self.station[2], self.gap_min, self.gap_max)) - results[tool] = (cx, cy, zp) - self.last['t%d' % tool] = (cx, cy, zp) + results[tool] = (center_x, center_y, z_trigger) + self.last['t%d' % tool] = (center_x, center_y, z_trigger) gcmd.respond_info("T%d: offset = (%.4f, %.4f, %.4f)" - % (tool, cx, cy, zp)) + % (tool, center_x, center_y, z_trigger)) if release: self._run('TOOLCHANGE_PARK') self._wait_moves() @@ -580,8 +607,8 @@ class FFToolOffset: # 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) + for tool, (center_x, center_y, z_trigger) in results.items(): + self.tools[tool].set_nozzle(center_x, center_y, z_trigger) # the app's exit block: heater off for the tool(s), Z15 for tool in tools: @@ -616,30 +643,32 @@ class FFToolOffset: # releaseFourExtruder: the carriage must be empty for TS. self._run('TOOLCHANGE_PARK') self._wait_moves() - cur = self.toolchange.get_status(self.reactor.monotonic()) - if cur.get('current_tool', -1) != -1 or not cur.get('state_ok'): + carriage = self.toolchange.get_status(self.reactor.monotonic()) + if carriage.get('current_tool', -1) != -1 \ + or not carriage.get('state_ok'): raise gcmd.error( "%s: carriage is not verifiably empty (%s) -- the station" " pass must run with no tool mounted" - % (self.name, cur.get('state_reason'))) + % (self.name, carriage.get('state_reason'))) elif park: raise gcmd.error("%s: [ff_toolchange] not loaded -- park the tool" " by hand and pass PARK=0." % self.name) self._run_plate_check(gcmd) - cyl_x, cyl_y = self._cylinder() + cylinder_x, cylinder_y = self._cylinder() gcmd.respond_info("station calibration, start %.3f, %.3f" - % (cyl_x, cyl_y)) + % (cylinder_x, cylinder_y)) expected_z = self.station[2] if self.station is not None else None def body(): - return self._two_pass(gcmd, cyl_x, cyl_y, self.z_target_station_2, - expected_z) - cx, cy, zp = self._with_accel_guard(gcmd, body) - self.last['station'] = (cx, cy, zp) - gcmd.respond_info("station = (%.4f, %.4f, %.4f)" % (cx, cy, zp)) + return self._two_pass(gcmd, cylinder_x, cylinder_y, + self.z_target_station_2, expected_z) + center_x, center_y, z_trigger = self._with_accel_guard(gcmd, body) + self.last['station'] = (center_x, center_y, z_trigger) + gcmd.respond_info("station = (%.4f, %.4f, %.4f)" + % (center_x, center_y, z_trigger)) if save: - self.set_station(cx, cy, zp) + self.set_station(center_x, center_y, z_trigger) self._refresh_toolchange(gcmd) gcmd.respond_info( "The SAVE_CONFIG command will update the printer config file" @@ -652,23 +681,24 @@ class FFToolOffset: cmd_TOOL_OFFSET_STATUS_help = "Show the configured nozzle/station positions" def cmd_TOOL_OFFSET_STATUS(self, gcmd): - cyl_x, cyl_y = self._cylinder() - lines = ["station start (cylinder_x/y): %.3f, %.3f" % (cyl_x, cyl_y)] - for t in self.tools: - if t.calibrated(): + cylinder_x, cylinder_y = self._cylinder() + lines = ["station start (cylinder_x/y): %.3f, %.3f" + % (cylinder_x, cylinder_y)] + for tool in self.tools: + if tool.calibrated(): lines.append("[ff_tool %d] nozzle %.4f, %.4f, %.4f" - % (t.index, t.nozzle[0], t.nozzle[1], - t.nozzle[2])) + % (tool.index, tool.nozzle[0], tool.nozzle[1], + tool.nozzle[2])) else: - lines.append("[ff_tool %d] nozzle NOT CALIBRATED" % t.index) + lines.append("[ff_tool %d] nozzle NOT CALIBRATED" % tool.index) if self.station is not None: lines.append("[%s] station %.4f, %.4f, %.4f" % ((self.name,) + self.station)) else: lines.append("[%s] station NOT CALIBRATED" % self.name) configfile = self.printer.lookup_object('configfile') - st = configfile.get_status(self.reactor.monotonic()) - if st.get('save_config_pending'): + configfile_status = configfile.get_status(self.reactor.monotonic()) + if configfile_status.get('save_config_pending'): lines.append("! unsaved calibration pending -- run SAVE_CONFIG") gcmd.respond_info("\n".join(lines)) @@ -687,38 +717,43 @@ class FFToolOffset: base_tool = (self.toolchange.offset_base if self.toolchange is not None else 0) lines = [] - for tool, (cx, cy, zp) in sorted(results.items()): + for tool, (center_x, center_y, z_trigger) in sorted(results.items()): lines.append("T%d measured: nozzle centre %.4f, %.4f" - " Z trigger %.4f" % (tool, cx, cy, zp)) - base = None + " Z trigger %.4f" + % (tool, center_x, center_y, z_trigger)) + base_nozzle = None if base_tool in results: - base = results[base_tool] + base_nozzle = results[base_tool] elif self.tools[base_tool].calibrated(): - base = self.tools[base_tool].nozzle + base_nozzle = self.tools[base_tool].nozzle z_station = self.station[2] if self.station is not None else None - if base is not None: + if base_nozzle is not None: lines.append("offsets a toolchange applies (X/Y vs T%d; Z %s):" % (base_tool, "absolute: nozzle_z - station_z + z_adjust" if z_station is not None else "vs T%d, + z_adjust" % base_tool)) - for tool, (cx, cy, zp) in sorted(results.items()): - za = self.tools[tool].z_adjust + for tool, (center_x, center_y, z_trigger) in sorted( + results.items()): + z_adjust = self.tools[tool].z_adjust if z_station is not None: - z_applied = za + (zp - z_station) + z_applied = z_adjust + (z_trigger - z_station) else: - z_applied = za + (zp - base[2]) + z_applied = z_adjust + (z_trigger - base_nozzle[2]) lines.append(" T%d: dX %+.4f dY %+.4f Z %+.4f" - % (tool, cx - base[0], cy - base[1], z_applied)) + % (tool, center_x - base_nozzle[0], + center_y - base_nozzle[1], z_applied)) gcmd.respond_info("\n".join(lines)) def get_status(self, eventtime): - out = {} - for k, v in self.last.items(): - out[k] = {'x': v[0], 'y': v[1], 'z': v[2]} - sx, sy, sz = self.station if self.station else (None, None, None) - return {'last': out, 'station_x': sx, 'station_y': sy, - 'station_z': sz} + measured = {} + for what, position in self.last.items(): + measured[what] = {'x': position[0], 'y': position[1], + 'z': position[2]} + station_x, station_y, station_z = (self.station if self.station + else (None, None, None)) + return {'last': measured, 'station_x': station_x, + 'station_y': station_y, 'station_z': station_z} def load_config(config): diff --git a/payload/klipper/extras/ff_toolchange.py b/payload/klipper/extras/ff_toolchange.py index 24af937..2f72471 100644 --- a/payload/klipper/extras/ff_toolchange.py +++ b/payload/klipper/extras/ff_toolchange.py @@ -86,15 +86,15 @@ ERR_RELEASE_STATE = 144 # E0144 state error after release verify # --------------------------------------------------------------------------- class _ToolchangerView: - def __init__(self, tc): - self.tc = tc + def __init__(self, toolchanger): + self.toolchanger = toolchanger def get_status(self, eventtime): - st = self.tc.get_status(eventtime) - cur = st['current_tool'] - if self.tc.changing: + own = self.toolchanger.get_status(eventtime) + mounted = own['current_tool'] + if self.toolchanger.changing: status = 'changing' - elif not st['state_ok']: + elif not own['state_ok']: status = 'error' else: status = 'ready' @@ -104,55 +104,57 @@ class _ToolchangerView: # and grab sensors, so the two are the same value by construction. # They are reported separately anyway, because a UI that only reads # detected_* must still see a tool. - name = 'T%d' % cur if cur >= 0 else None + name = 'T%d' % mounted if mounted >= 0 else None return {'name': 'toolchanger', 'status': status, - 'tool_number': cur, + 'tool_number': mounted, 'tool_numbers': list(range(EXTRUDER_COUNT)), 'tool_names': ['T%d' % i for i in range(EXTRUDER_COUNT)], 'tool': name, 'detected_tool': name, - 'detected_tool_number': cur, + 'detected_tool_number': mounted, 'has_detection': True, - 'state_reason': st['state_reason'], - 'print_offset_ready': st['print_offset_ready']} + 'state_reason': own['state_reason'], + 'print_offset_ready': own['print_offset_ready']} class _ToolView: - def __init__(self, tc, index): - self.tc = tc + def __init__(self, toolchanger, index): + self.toolchanger = toolchanger self.index = index def get_status(self, eventtime): - tc = self.tc - cur = tc.get_status(eventtime)['current_tool'] - t = tc.tools[self.index] + toolchanger = self.toolchanger + mounted = toolchanger.get_status(eventtime)['current_tool'] + tool = toolchanger.tools[self.index] # klipper-toolchanger's detect_state: is THIS tool seen on the # carriage -- our per-tool grab sensor. 'unavailable' if unreadable. # 'mounted' is upstream's spelling of present (DETECT_PRESENT). try: - detect = ('mounted' if tc._sensor(tc.grab_sensors[self.index], - eventtime) else 'absent') + detect = ('mounted' + if toolchanger._sensor( + toolchanger.grab_sensors[self.index], eventtime) + else 'absent') except (FFToolchangeError, IndexError): detect = 'unavailable' return {'tool_number': self.index, 'name': 'T%d' % self.index, 'toolchanger': 'toolchanger', - 'active': cur == self.index, - 'mounted': cur == self.index, + 'active': mounted == self.index, + 'mounted': mounted == self.index, 'detect_state': detect, - 'extruder': t.extruder_name, + 'extruder': tool.extruder_name, # In Klipper the extruder object IS its heater, and this # machine has no separate [extruder_stepper] sections, so # upstream's third name has nothing to point at. - 'heater': t.extruder_name, + 'heater': tool.extruder_name, 'extruder_stepper': None, - 'fan': tc.part_fan, + 'fan': toolchanger.part_fan, # the differences actually applied on a grab - 'gcode_x_offset': tc.off_x[self.index], - 'gcode_y_offset': tc.off_y[self.index], - 'gcode_z_offset': tc.off_z[self.index], - 'calibrated': t.calibrated()} + 'gcode_x_offset': toolchanger.offset_x[self.index], + 'gcode_y_offset': toolchanger.offset_y[self.index], + 'gcode_z_offset': toolchanger.offset_z[self.index], + 'calibrated': tool.calibrated()} class FFToolchangeError(Exception): @@ -189,7 +191,7 @@ class FFToolchange: # and docs/notes/40-offsets.md. self.offset_base = config.getint('offset_base', 0, minval=0, maxval=EXTRUDER_COUNT - 1) - self.off_x = self.off_y = self.off_z = [0.0] * EXTRUDER_COUNT + self.offset_x = self.offset_y = self.offset_z = [0.0] * EXTRUDER_COUNT self.refresh_offsets() # True while a T/TOOLCHANGE sequence is running (reported as # toolchanger.status = 'changing'). @@ -372,28 +374,32 @@ class FFToolchange: tools = self.tools z_station = self._station_z() base_ok = tools[base].calibrated() - bx, by, bz = tools[base].nozzle if base_ok else (0.0, 0.0, 0.0) - xs, ys, zs = [], [], [] - for t in tools: - if t.calibrated() and base_ok: - xs.append(t.nozzle[0] - bx) - ys.append(t.nozzle[1] - by) + base_x, base_y, base_z = (tools[base].nozzle if base_ok + else (0.0, 0.0, 0.0)) + x_offsets, y_offsets, z_offsets = [], [], [] + for tool in tools: + if tool.calibrated() and base_ok: + x_offsets.append(tool.nozzle[0] - base_x) + y_offsets.append(tool.nozzle[1] - base_y) else: - xs.append(0.0) - ys.append(0.0) - if t.calibrated() and z_station is not None: - zs.append(t.z_adjust + (t.nozzle[2] - z_station)) - elif t.calibrated() and base_ok: - zs.append(t.z_adjust + (t.nozzle[2] - bz)) + x_offsets.append(0.0) + y_offsets.append(0.0) + if tool.calibrated() and z_station is not None: + z_offsets.append(tool.z_adjust + (tool.nozzle[2] - z_station)) + elif tool.calibrated() and base_ok: + z_offsets.append(tool.z_adjust + (tool.nozzle[2] - base_z)) else: - zs.append(t.z_adjust) - return xs, ys, zs + z_offsets.append(tool.z_adjust) + return x_offsets, y_offsets, z_offsets def refresh_offsets(self, gcmd=None): """Re-derive after a calibration changed an [ff_tool] live.""" - fx, fy, fz = self._derive_offsets(self.offset_base) - changed = (fx, fy, fz) != (self.off_x, self.off_y, self.off_z) - self.off_x, self.off_y, self.off_z = fx, fy, fz + derived_x, derived_y, derived_z = self._derive_offsets( + self.offset_base) + changed = ((derived_x, derived_y, derived_z) + != (self.offset_x, self.offset_y, self.offset_z)) + self.offset_x, self.offset_y, self.offset_z = (derived_x, derived_y, + derived_z) if gcmd is not None: gcmd.respond_info("ff_toolchange: offsets %s" % ("updated" if changed else "unchanged")) @@ -414,10 +420,11 @@ class FFToolchange: Reset the remembered term to what is actually applied (nothing), then re-apply the mounted tool's offsets so Z=0 is the bed again. With the term at 0 and the live offset at 0 the recovered babystep is 0, so - this applies exactly off_z[tool] -- the frame a fresh grab would set. + this applies exactly offset_z[tool] -- the frame a fresh grab + would set. """ self._z_tool_term = 0.0 - tool, _why = self._current_tool_or_none() + tool, _reason = self._current_tool_or_none() if tool is None or tool < 0: return False self._apply_tool_diff_offsets(tool) @@ -428,14 +435,14 @@ class FFToolchange: return True def uncalibrated_tools(self): - return [t.index for t in self.tools if not t.calibrated()] + return [tool.index for tool in self.tools if not tool.calibrated()] def _station_z(self): """station_z from [ff_tool_offset] (STATION_CALIBRATE), or None.""" - st = self.printer.lookup_object('ff_tool_offset', None) - if st is None or st.station is None: + offsets = self.printer.lookup_object('ff_tool_offset', None) + if offsets is None or offsets.station is None: return None - return st.station[2] + return offsets.station[2] # ---------------- plumbing ---------------- @@ -456,24 +463,25 @@ class FFToolchange: missing.append("gcode_button %s" % name) for tool in range(EXTRUDER_COUNT): - ename = self._extruder_name(tool) - if self.printer.lookup_object(ename, None) is None: - missing.append(ename) + extruder_name = self._extruder_name(tool) + if self.printer.lookup_object(extruder_name, None) is None: + missing.append(extruder_name) # gcode_macro objects keep the section name's casing, but the command # each registers is name.upper() (gcode_macro.py: alias = name.upper()) # -- so compare case-insensitively against the registered macros. macros = set() - for oname, _obj in self.printer.lookup_objects(module='gcode_macro'): - parts = oname.split(None, 1) - if len(parts) == 2: - macros.add(parts[1].upper()) + for object_name, _obj in self.printer.lookup_objects( + module='gcode_macro'): + section_words = object_name.split(None, 1) + if len(section_words) == 2: + macros.add(section_words[1].upper()) - for mname in (self.grab_macro, self.grab2_macro, - self.release_macro, self.stop_macro): - cmd = mname.split()[0] - if cmd.upper() not in macros: - missing.append("gcode_macro %s" % cmd) + for macro_name in (self.grab_macro, self.grab2_macro, + self.release_macro, self.stop_macro): + command = macro_name.split()[0] + if command.upper() not in macros: + missing.append("gcode_macro %s" % command) self.runout_switch = self._resolve_runout_sensors( 'filament_switch_sensor', self.runout_switch_prefix, missing) @@ -505,7 +513,7 @@ class FFToolchange: # Mirror the sensors to whatever is on the carriage after a # restart (the app re-arms at print start only; arming outside a # print is harmless -- _FF_RUNOUT ignores it unless printing). - tool, _why = self._current_tool_or_none() + tool, _reason = self._current_tool_or_none() if tool is None or tool < 0: self._disarm_runout() else: @@ -688,16 +696,16 @@ class FFToolchange: """Non-throwing variant, for reporting only.""" try: return self._derive_current_tool(eventtime) - except FFToolchangeError as e: - return None, str(e) + except FFToolchangeError as err: + return None, str(err) def _dock(self, tool): - t = self.tools[tool] - if not t.has_dock(): + tool_object = self.tools[tool] + if not tool_object.has_dock(): raise FFToolchangeError( "T%d has no dock position ([ff_tool %d] dock_x/dock_y) --" " run FF_IMPORT_FIRMWARE_CONFIG and SAVE_CONFIG" % (tool, tool)) - return t.dock_x + self.x_correction, t.dock_y + return tool_object.dock_x + self.x_correction, tool_object.dock_y def _extruder_name(self, tool): return self.tools[tool].extruder_name @@ -710,8 +718,8 @@ class FFToolchange: def _parse_axes(raw, error, what): """'xyz' -> 'XYZ', rejecting anything that is not an axis letter.""" axes = ''.join(sorted(set(raw.strip().upper()))) - bad = [a for a in axes if a not in 'XYZ'] - if bad: + not_axis_letters = [letter for letter in axes if letter not in 'XYZ'] + if not_axis_letters: raise error("%s: expected letters from XYZ, got '%s'" % (what, raw)) return axes @@ -722,8 +730,8 @@ class FFToolchange: def _capture_position(self): """The G-code position the change is about to disturb.""" - gm = self.printer.lookup_object('gcode_move') - return list(gm.get_status()['gcode_position']) + gcode_move = self.printer.lookup_object('gcode_move') + return list(gcode_move.get_status()['gcode_position']) def _restore_position(self, axes, pos): """Put the toolhead back where the change found it. @@ -743,15 +751,15 @@ class FFToolchange: """ if not axes: return - xy = ' '.join('%s%.3f' % (a, pos[i]) - for i, a in enumerate('XY') if a in axes) + xy_move = ' '.join('%s%.3f' % (letter, pos[i]) + for i, letter in enumerate('XY') if letter in axes) # The sequence has already put the modal state back; borrow it and # return it rather than leaving G90 and the restore feed behind. self._run('SAVE_GCODE_STATE NAME=_ff_restore_axis') try: self._run('G90') - if xy: - self._run('G1 %s F%d' % (xy, self.restore_feed)) + if xy_move: + self._run('G1 %s F%d' % (xy_move, self.restore_feed)) if 'Z' in axes: self._run('G1 Z%.3f F%d' % (pos[2], self.restore_feed)) finally: @@ -775,15 +783,15 @@ class FFToolchange: restores the pre-sequence mode and feedrate.""" # gcode_move always exists -- toolhead.py:297 loads it # unconditionally with its other standard modules. - st = self.printer.lookup_object('gcode_move').get_status() - absolute, speed = (st['absolute_coordinates'], st['speed']) + modal = self.printer.lookup_object('gcode_move').get_status() + absolute, speed = (modal['absolute_coordinates'], modal['speed']) - accel = self.accel_restore or self._current_max_accel() + restore_accel = self.accel_restore or self._current_max_accel() try: yield finally: self._run(self.stop_macro) - self._run('SET_VELOCITY_LIMIT ACCEL=%d' % accel) + self._run('SET_VELOCITY_LIMIT ACCEL=%d' % restore_accel) if not absolute: self._run('G91') @@ -807,7 +815,7 @@ class FFToolchange: def _grab(self, tool): """Port of CommMgr::doGrabExtruderLatest @0x7a8190.""" - dx, dy = self._dock(tool) + dock_x, dock_y = self._dock(tool) # Precheck: the target must be detected in its dock (20 x 50 ms). # No motion at all on failure. @@ -823,7 +831,7 @@ class FFToolchange: self._run('SET_GCODE_OFFSET X=0 Y=0 MOVE=1 MOVE_SPEED=100') self._run('G90') self._run('G1 X%.3f F%d' % (self.x_safe, self.fast_feed)) - self._run('G1 Y%.3f' % dy) + self._run('G1 Y%.3f' % dock_y) self._run('G1 X%.3f' % self.x_approach) # Up to 3 attempts; within each, poll up to 1 s for the grab @@ -835,7 +843,7 @@ class FFToolchange: # back-off to x_approach below is undone before the next # round of polling. Without it, attempts 2 and 3 poll from # the backed-off position and can never mate. - self._run('G1 X%.3f F%d' % (dx, self.slow_feed)) + self._run('G1 X%.3f F%d' % (dock_x, self.slow_feed)) self._wait_moves() # the app sleeps before its first poll @@ -847,7 +855,7 @@ class FFToolchange: # The pullback feed is the app's literal F4800 # (@0x7a9074), NOT the calibrated slow feed. self._run('G1 X%.3f F%d' - % (dx - self.grab_pullback, PULLBACK_FEED)) + % (dock_x - self.grab_pullback, PULLBACK_FEED)) self._run(self.grab2_macro) break @@ -869,10 +877,10 @@ class FFToolchange: # Verify: the tool must have LEFT its dock and the grab sensor # must be engaged. (doGrabExtruderLatest: !inLocation && grab) - ok = self._poll_until( + triggered = self._poll_until( lambda: (not self._in_location(tool)) and self._grab_sensor(), VERIFY_TIMEOUT) - if not ok: + if not triggered: raise FFToolchangeError( "T%d pickup could not be verified: in_dock=%s" " grab_sensor=%s (firmware error E%04d)" @@ -910,7 +918,7 @@ class FFToolchange: MOTOR_RELEASE it never supervises (it re-issues before checking its counter); we stop at three rather than energise the motor with no one watching.""" - dx, dy = self._dock(tool) + dock_x, dock_y = self._dock(tool) # Sensors off first (changeExtruderChannel @0x79750c disarms before # the head swap): nothing the carriage does at the dock may fire a @@ -935,14 +943,14 @@ class FFToolchange: # loop. There is NO X280 stage here -- that is grab-only; the # release staging point is dockX-10 below. self._run('G1 X%.3f F%d' % (self.x_safe, self.fast_feed)) - self._run('G1 Y%.3f' % dy) + self._run('G1 Y%.3f' % dock_y) for attempt in range(RELEASE_ATTEMPTS): # The app emits G1 X with NO F (inheriting the # modal feed: fast on attempt 1, the slow feed on retries) # and the final mate at the release slow feed. - self._run('G1 X%.3f' % (dx - RELEASE_STAGE_BACKOFF)) - self._run('G1 X%.3f F%d' % (dx, self.release_slow_feed)) + self._run('G1 X%.3f' % (dock_x - RELEASE_STAGE_BACKOFF)) + self._run('G1 X%.3f F%d' % (dock_x, self.release_slow_feed)) self._wait_moves() # Unlock only once the dock sensor confirms the tool has @@ -988,10 +996,10 @@ class FFToolchange: # Verify: tool in its dock AND nothing held. # (doReleaseExtruderLatest: inLocation && !grab) - ok = self._poll_until( + triggered = self._poll_until( lambda: self._in_location(tool) and not self._grab_sensor(), VERIFY_TIMEOUT) - if not ok: + if not triggered: raise FFToolchangeError( "state error after releasing T%d: in_dock=%s" " grab_sensor=%s (firmware error E%04d)" @@ -1049,8 +1057,8 @@ class FFToolchange: # on the right hotend for exactly that reason. if resume is not None: self._restore_position(restore_axis, resume) - except FFToolchangeError as e: - raise gcmd.error(str(e)) + except FFToolchangeError as err: + raise gcmd.error(str(err)) except self.printer.command_error: # A raw Klipper error (move out of range, shutdown, macro fault) # escaped the sequence. The finally-clauses restored accel, @@ -1073,16 +1081,17 @@ class FFToolchange: """Apply this tool's offsets after a successful grab. Port of CommMgr::setGrabGcodeOffsetMgr(tool, onlyZ=false). - off_x/off_y are small tool-to-tool DIFFERENCES against the base - tool; off_z is this tool's ABSOLUTE gap to the bed plane (~+3.2 mm, + offset_x/offset_y are small tool-to-tool DIFFERENCES against the base + tool; offset_z is this tool's ABSOLUTE gap to the bed plane (~+3.2 mm, see _derive_offsets). Unlike the app, which sets that gap once per print (setZOffsetWhenPrint) and carries it across changes, every grab establishes it here -- so the only thing carried is whatever sits on top of it: TOOLCHANGE_SET_PRINT_OFFSET's job terms and the user's live babystep. - new Z = off_z[new] + (old Z - off_z[old]) - = [t_new_z - z_station + zoff[new]] + job terms + babystep + new Z = offset_z[new] + (old Z - offset_z[old]) + = [t_new_z - z_station + z_adjust[new]] + job terms + + babystep The app sends two commands, different move speeds, 3-decimal formatting (BaseFunction::float_to_string(v, 3)): @@ -1105,9 +1114,9 @@ class FFToolchange: to plain tool diffs in the raw frame, which is what the stock app's calibration flows expect.""" babystep = self._gcode_z_offset() - self._z_tool_term - z_term = self.off_z[tool] + z_term = self.offset_z[tool] self._run('SET_GCODE_OFFSET X=%.3f Y=%.3f MOVE=1 MOVE_SPEED=100' - % (self.off_x[tool], self.off_y[tool])) + % (self.offset_x[tool], self.offset_y[tool])) self._run('SET_GCODE_OFFSET Z=%.3f MOVE=1 MOVE_SPEED=40' % (z_term + babystep)) self._z_tool_term = z_term @@ -1138,7 +1147,8 @@ class FFToolchange: against the fixed under-bed sensor. Derivation: docs/notes/40-offsets.md. The gap term (nozzle_z - station_z + z_adjust) is already the grab - offset (off_z, applied by every T); this command re-asserts it and + offset (offset_z, applied by every T); this command re-asserts + it and adds the job terms, which the next _apply_tool_diff_offsets then carries as "babystep" -- the app re-adds m_zOffset on every grab. END/CANCEL must reset with SET_GCODE_OFFSET Z=0 MOVE=1 (app exit @@ -1148,7 +1158,7 @@ class FFToolchange: layer = gcmd.get_float('LAYER', 0.) tool = gcmd.get_int('TOOL', -1, minval=-1, maxval=EXTRUDER_COUNT - 1) if tool < 0: - current, _why = self._current_tool_or_none() + current, _reason = self._current_tool_or_none() tool = current if current is not None and current >= 0 else 0 z_station = self._station_z() @@ -1161,28 +1171,29 @@ class FFToolchange: " several mm too low; NOT printing is the safe choice. Run" " STATION_CALIBRATE / TOOL_OFFSET_CALIBRATE (or" " FF_IMPORT_FIRMWARE_CONFIG) and SAVE_CONFIG." % tool) - tools = [t.nozzle or (0.0, 0.0, 0.0) for t in self.tools] - zoff = [t.z_adjust for t in self.tools] - z = tools[tool][2] - z_station + (nozzle - 120.0) * temp_coeff + nozzles = [tool_object.nozzle or (0.0, 0.0, 0.0) + for tool_object in self.tools] + z_adjusts = [tool_object.z_adjust for tool_object in self.tools] + z = nozzles[tool][2] - z_station + (nozzle - 120.0) * temp_coeff if bed >= 100.0: z += 0.08 int_layer = int(layer * 100.0) if 0 < int_layer <= 10: z += -0.06 - z += zoff[tool] + z += z_adjusts[tool] gcmd.respond_info( "print Z offset for T%d: %.3f (gap %.3f, temp %+.3f," " bed %+.2f, layer %+.2f, user %+.3f)" - % (tool, z, tools[tool][2] - z_station, + % (tool, z, nozzles[tool][2] - z_station, (nozzle - 120.0) * temp_coeff, 0.08 if bed >= 100.0 else 0.0, - -0.06 if 0 < int_layer <= 10 else 0.0, zoff[tool])) + -0.06 if 0 < int_layer <= 10 else 0.0, z_adjusts[tool])) self._run('SET_GCODE_OFFSET X=%.3f Y=%.3f Z=%.3f' ' MOVE=1 MOVE_SPEED=100' - % (self.off_x[tool], self.off_y[tool], z)) - # off_z[tool] is the gap + z_adjust part of z (station_z and the + % (self.offset_x[tool], self.offset_y[tool], z)) + # offset_z[tool] is the gap + z_adjust part of z (station_z and the # calibration were checked above); the remainder is the job terms. - self._z_tool_term = self.off_z[tool] + self._z_tool_term = self.offset_z[tool] cmd_TOOL_Z_ADJUST_help = ( "Per-tool persistent Z correction: TOOL_Z_ADJUST TOOL=<0..3> " @@ -1202,15 +1213,15 @@ class FFToolchange: if (adjust is None) == (value is None): raise gcmd.error("TOOL_Z_ADJUST: give exactly one of ADJUST= or" " VALUE=") - t = self.tools[tool] - old = t.z_adjust - new = value if value is not None else old + adjust - t.set_z_adjust(new) + tool_object = self.tools[tool] + previous = tool_object.z_adjust + updated = value if value is not None else previous + adjust + tool_object.set_z_adjust(updated) self.refresh_offsets() applied = "" try: self._wait_moves() - current, _why = self._current_tool_or_none() + current, _reason = self._current_tool_or_none() except FFToolchangeError: current = None if current == tool: @@ -1219,29 +1230,29 @@ class FFToolchange: gcmd.respond_info( "T%d z_adjust %.3f -> %.3f%s. The SAVE_CONFIG command will update" " the printer config file and restart the printer." - % (tool, old, new, applied)) + % (tool, previous, updated, applied)) # ---------------- klipper-toolchanger command aliases ---------------- def _tool_arg(self, gcmd, required=True): """klipper-toolchanger accepts T= or TOOL=.""" - t = gcmd.get_int('T', None) - if t is None: + tool = gcmd.get_int('T', None) + if tool is None: name = gcmd.get('TOOL', None) if name is not None: name = name.strip() if name.upper().startswith('T') and name[1:].isdigit(): - t = int(name[1:]) + tool = int(name[1:]) else: raise gcmd.error("TOOL must be T0..T%d, got '%s'" % (EXTRUDER_COUNT - 1, name)) - if t is None: + if tool is None: if required: raise gcmd.error("T= or TOOL=T is required") return None - if not 0 <= t < EXTRUDER_COUNT: + if not 0 <= tool < EXTRUDER_COUNT: raise gcmd.error("T must be 0..%d" % (EXTRUDER_COUNT - 1)) - return t + return tool cmd_SELECT_TOOL_help = ("Select a tool (T= | TOOL=T); same as" " T. RESTORE_AXIS= returns the toolhead") @@ -1254,12 +1265,12 @@ class FFToolchange: " the toolhead") def cmd_UNSELECT_TOOL(self, gcmd): - t = self._tool_arg(gcmd, required=False) - if t is not None: - cur, _why = self._current_tool_or_none() - if cur != t: + tool = self._tool_arg(gcmd, required=False) + if tool is not None: + mounted, _reason = self._current_tool_or_none() + if mounted != tool: raise gcmd.error("UNSELECT_TOOL: T%d is not the mounted tool" - " (current %s)" % (t, cur)) + " (current %s)" % (tool, mounted)) self.cmd_TOOLCHANGE_PARK(gcmd) cmd_INITIALIZE_TOOLCHANGER_help = ( @@ -1267,11 +1278,11 @@ class FFToolchange: def cmd_INITIALIZE_TOOLCHANGER(self, gcmd): self._wait_moves() - cur, why = self._current_tool_or_none() - if cur is None: - raise gcmd.error("toolchanger state not derivable: %s" % why) + mounted, reason = self._current_tool_or_none() + if mounted is None: + raise gcmd.error("toolchanger state not derivable: %s" % reason) gcmd.respond_info("toolchanger ready, tool_number=%d (%s)" - % (cur, why)) + % (mounted, reason)) cmd_ASSIGN_TOOL_help = "Not supported on this toolchanger" @@ -1291,10 +1302,11 @@ class FFToolchange: a UI knows it is heating T2, not that T2 means [extruder2].""" tool = self._tool_arg(gcmd, required=False) if tool is None: - tool, why = self._current_tool_or_none() + tool, reason = self._current_tool_or_none() if tool is None or tool < 0: raise gcmd.error("SET_TOOL_TEMPERATURE: no tool mounted, so" - " T= or TOOL=T is required (%s)" % why) + " T= or TOOL=T is required (%s)" + % reason) target = gcmd.get_float('TARGET', 0.) heater = self._extruder_name(tool) self._run('SET_HEATER_TEMPERATURE HEATER=%s TARGET=%.1f' @@ -1317,17 +1329,19 @@ class FFToolchange: gcmd.get_int('ASYNC', 0) expect = self._tool_arg(gcmd, required=False) self._wait_moves() - cur, why = self._current_tool_or_none() - if cur is None: + mounted, reason = self._current_tool_or_none() + if mounted is None: raise gcmd.error("VERIFY_TOOL_DETECTED: toolchanger state not" - " derivable: %s" % why) - if expect is not None and cur != expect: + " derivable: %s" % reason) + if expect is not None and mounted != expect: raise gcmd.error("VERIFY_TOOL_DETECTED: expected T%d, sensors say" " %s (%s)" % (expect, - 'T%d' % cur if cur >= 0 else 'no tool', why)) + 'T%d' % mounted if mounted >= 0 + else 'no tool', reason)) gcmd.respond_info("detected %s (%s)" - % ('T%d' % cur if cur >= 0 else 'no tool', why)) + % ('T%d' % mounted if mounted >= 0 else 'no tool', + reason)) cmd_SELECT_TOOL_ERROR_help = "Abort the running script: a tool change failed" @@ -1344,9 +1358,10 @@ class FFToolchange: def cmd_FF_RUNOUT_ARM(self, gcmd): tool = gcmd.get_int('TOOL', -1) if tool < 0: - tool, why = self._current_tool_or_none() + tool, reason = self._current_tool_or_none() if tool is None or tool < 0: - raise gcmd.error("FF_RUNOUT_ARM: no tool mounted (%s)" % why) + raise gcmd.error("FF_RUNOUT_ARM: no tool mounted (%s)" + % reason) elif tool >= EXTRUDER_COUNT: raise gcmd.error("FF_RUNOUT_ARM: TOOL must be 0..%d" % (EXTRUDER_COUNT - 1)) @@ -1367,57 +1382,64 @@ class FFToolchange: def cmd_TOOLCHANGE_STATUS(self, gcmd): self._wait_moves() - tool, why = self._current_tool_or_none() + tool, reason = self._current_tool_or_none() if tool is None: - lines = ["current_tool=UNKNOWN", " ! %s" % why] + lines = ["current_tool=UNKNOWN", " ! %s" % reason] else: - lines = ["current_tool=%d (%s)" % (tool, why)] + lines = ["current_tool=%d (%s)" % (tool, reason)] lines.append(" (derived from the dock sensors; nothing is stored)") if self.runout_switch or self.runout_motion: lines.append(" runout sensors armed: %s" % (", ".join(self._armed_sensors()) or "none")) - for i, n in enumerate(self.dock_sensors): + for i, sensor in enumerate(self.dock_sensors): try: lines.append(" T%d in dock (%s): %s" - % (i, n, self._sensor(n))) - except FFToolchangeError as e: - lines.append(" T%d (%s): %s" % (i, n, e)) - for n in self.grab_sensors: + % (i, sensor, self._sensor(sensor))) + except FFToolchangeError as err: + lines.append(" T%d (%s): %s" % (i, sensor, err)) + for sensor in self.grab_sensors: try: - lines.append(" grab (%s): %s" % (n, self._sensor(n))) - except FFToolchangeError as e: - lines.append(" grab (%s): %s" % (n, e)) + lines.append(" grab (%s): %s" + % (sensor, self._sensor(sensor))) + except FFToolchangeError as err: + lines.append(" grab (%s): %s" % (sensor, err)) lines.append("geometry ([ff_tool n] / [ff_toolchange]):") - for t in self.tools: - if t.calibrated(): + for tool_object in self.tools: + if tool_object.calibrated(): lines.append(" T%d nozzle (%.4f, %.4f, %.4f) z_adjust %+.3f" - % (t.index, t.nozzle[0], t.nozzle[1], - t.nozzle[2], t.z_adjust)) + % (tool_object.index, tool_object.nozzle[0], + tool_object.nozzle[1], tool_object.nozzle[2], + tool_object.z_adjust)) else: lines.append(" T%d nozzle NOT CALIBRATED" " (zero X/Y, z_adjust only in Z)" - % t.index) - sz = self._station_z() + % tool_object.index) + station_z = self._station_z() lines.append(" station_z %s" - % ("%.4f" % sz if sz is not None else "NOT CALIBRATED")) + % ("%.4f" % station_z if station_z is not None + else "NOT CALIBRATED")) def series_line(option, values): lines.append(" %-14s[%s]" - % (option, ", ".join("%.4f" % v for v in values))) + % (option, + ", ".join("%.4f" % value for value in values))) - series_line('dock_x', [t.dock_x if t.has_dock() else float('nan') - for t in self.tools]) - series_line('dock_y', [t.dock_y if t.has_dock() else float('nan') - for t in self.tools]) - missing = [t.index for t in self.tools if not t.has_dock()] + series_line('dock_x', + [tool_object.dock_x if tool_object.has_dock() + else float('nan') for tool_object in self.tools]) + series_line('dock_y', + [tool_object.dock_y if tool_object.has_dock() + else float('nan') for tool_object in self.tools]) + missing = [tool_object.index for tool_object in self.tools + if not tool_object.has_dock()] if missing: lines.append(" ! no dock position for T%s -- run" " FF_IMPORT_FIRMWARE_CONFIG and SAVE_CONFIG" % ", T".join(str(i) for i in missing)) - series_line('offset_z', self.off_z) - series_line('offset_x', self.off_x) - series_line('offset_y', self.off_y) + series_line('offset_z', self.offset_z) + series_line('offset_x', self.offset_x) + series_line('offset_y', self.offset_y) lines.append(" offset_base T%d" % self.offset_base) lines.append(" x_correction %.4f" % self.x_correction) lines.append(" fast_feed %d" % self.fast_feed) @@ -1433,19 +1455,19 @@ class FFToolchange: resume = self._capture_position() if restore_axis else None try: self._wait_moves() - current, why = self._derive_current_tool() - except FFToolchangeError as e: - raise gcmd.error(str(e)) + current, reason = self._derive_current_tool() + except FFToolchangeError as err: + raise gcmd.error(str(err)) if current < 0: - gcmd.respond_info("no tool mounted (%s)" % why) + gcmd.respond_info("no tool mounted (%s)" % reason) return try: self._ensure_homed('xy') self._release(current) if resume is not None: self._restore_position(restore_axis, resume) - except FFToolchangeError as e: - raise gcmd.error(str(e)) + except FFToolchangeError as err: + raise gcmd.error(str(err)) def print_offset_ready(self, tool=None): """Can TOOLCHANGE_SET_PRINT_OFFSET succeed? Needs station_z and @@ -1457,12 +1479,13 @@ class FFToolchange: return self.tools[tool].calibrated() def get_status(self, eventtime): - tool, why = self._current_tool_or_none(eventtime) + tool, reason = self._current_tool_or_none(eventtime) return {'current_tool': -1 if tool is None else tool, 'state_ok': tool is not None, - 'state_reason': why, - 'calibrated_tools': [t.index for t in self.tools - if t.calibrated()], + 'state_reason': reason, + 'calibrated_tools': [tool_object.index + for tool_object in self.tools + if tool_object.calibrated()], 'station_z': self._station_z(), # True when every tool and the station are calibrated, i.e. # a print's Z frame can be established for any tool. diff --git a/payload/moonraker-preflight.py b/payload/moonraker-preflight.py index 5145556..1a18eb3 100755 --- a/payload/moonraker-preflight.py +++ b/payload/moonraker-preflight.py @@ -36,17 +36,18 @@ def main() -> int: print("usage: moonraker-preflight.py ") return 1 sys.path.insert(0, sys.argv[1]) - conf = sys.argv[2] if len(sys.argv) > 2 else "/usr/data/config/moonraker.conf" + conf_path = (sys.argv[2] if len(sys.argv) > 2 + else "/usr/data/config/moonraker.conf") # Importing the server module first is itself a check, and it is where the # component list comes from. try: - import moonraker.server as srv + import moonraker.server as server except Exception as exc: print("moonraker.server does not import: %r" % (exc,)) return 1 - names = list(getattr(srv, "CORE_COMPONENTS", [])) + names = list(getattr(server, "CORE_COMPONENTS", [])) # Whatever the user has configured gets loaded too, so it gets checked too. # A section that is not a component (e.g. [server]) is skipped below. # Follow [include ...] too. moonraker.conf is mod-owned and ends with @@ -62,20 +63,21 @@ def main() -> int: except OSError: return for line in lines: - m = re.match(r"\s*\[\s*([A-Za-z0-9_]+)", line) - if not m: + section = re.match(r"\s*\[\s*([A-Za-z0-9_]+)", line) + if not section: continue - if m.group(1) == "include": - inc = re.match(r"\s*\[\s*include\s+([^\]]+?)\s*\]", line) - if inc and depth < 3: - base = os.path.dirname(os.path.abspath(path)) - for sub in sorted(glob.glob(os.path.join(base, - inc.group(1)))): - scan(sub, depth + 1) + if section.group(1) == "include": + include = re.match(r"\s*\[\s*include\s+([^\]]+?)\s*\]", + line) + if include and depth < 3: + including_dir = os.path.dirname(os.path.abspath(path)) + for included in sorted(glob.glob( + os.path.join(including_dir, include.group(1)))): + scan(included, depth + 1) continue - names.append(m.group(1)) + names.append(section.group(1)) - scan(conf) + scan(conf_path) failures = [] for name in dict.fromkeys(names): # de-duplicate, keep order diff --git a/payload/run-append.sh b/payload/run-append.sh index bd7c4b9..bd21d62 100644 --- a/payload/run-append.sh +++ b/payload/run-append.sh @@ -19,11 +19,11 @@ for c in /usr/data/update/anvil.tar.xz /mnt/anvil.tar.xz $WORK_DIR/anvil.tar.xz; done if [ -n "$MODTAR" ]; then - NEED=`ls -l "$MODTAR" | tr -s ' ' | cut -d' ' -f5` - NEED=$((NEED / 1024 * 4)) # xz payload expands ~3-4x - FREE=`df /usr/data | tail -1 | tr -s ' ' | cut -d' ' -f4` - echo "mod payload: $MODTAR (need ~${NEED}KB, free ${FREE}KB)" - if [ "${FREE:-0}" -lt "$NEED" ]; then + NEED_KB=`ls -l "$MODTAR" | tr -s ' ' | cut -d' ' -f5` + NEED_KB=$((NEED_KB / 1024 * 4)) # xz payload expands ~3-4x + FREE_KB=`df /usr/data | tail -1 | tr -s ' ' | cut -d' ' -f4` + echo "mod payload: $MODTAR (need ~${NEED_KB}KB, free ${FREE_KB}KB)" + if [ "${FREE_KB:-0}" -lt "$NEED_KB" ]; then echo "!! not enough space on /usr/data -- skipping mod payload" else # Keep user-editable state; replace everything we own. @@ -62,12 +62,12 @@ sync # whose Moonraker did not survive an update has no web UI at all, and the # screen would be the only way to notice. if [ -d $MODDIR/moonraker ]; then - MRROOT=/usr/prog/moonraker/moonraker - if [ -d $MRROOT/moonraker ]; then + MOONRAKER_ROOT=/usr/prog/moonraker/moonraker + if [ -d $MOONRAKER_ROOT/moonraker ]; then # Both trees exist at once during the swap; /usr/prog is the small # firmware partition, so check there is room before starting. - NEED=`du -sk $MODDIR/moonraker | cut -f1` - FREE=`df /usr/prog | tail -1 | tr -s ' ' | cut -d' ' -f4` + NEED_KB=`du -sk $MODDIR/moonraker | cut -f1` + FREE_KB=`df /usr/prog | tail -1 | tr -s ' ' | cut -d' ' -f4` # PRE-FLIGHT: ask THIS printer's python whether it can load the tree # before anything is moved. moonraker-preflight.py explains what it # imports and why; the short version is that it uses Moonraker's own @@ -80,41 +80,43 @@ if [ -d $MODDIR/moonraker ]; then # is re-run by hand from ssh, where none of that is set -- and a check # that fails for a missing libsodium would condemn a perfectly good # build. Same list app_startup.sh uses. - MRPY=/usr/prog/Python-3.8.2/bin/python3 - MRIMP=0 - if [ -x $MRPY ] && [ -f $MODDIR/moonraker-preflight.py ]; then + MOONRAKER_PYTHON=/usr/prog/Python-3.8.2/bin/python3 + MOONRAKER_PREFLIGHT_FAILED=0 + if [ -x $MOONRAKER_PYTHON ] && [ -f $MODDIR/moonraker-preflight.py ]; then ( PATH=$PATH:/usr/prog/Python-3.8.2/bin - for d in /usr/prog/Python-3.8.2/lib /usr/prog/openssl-1.0.2d/lib \ + for libdir in /usr/prog/Python-3.8.2/lib \ + /usr/prog/openssl-1.0.2d/lib \ /usr/prog/curl-7.55.1/lib /usr/prog/ffmpeg-402/lib \ /usr/prog/x264/lib /usr/prog/libffi-3.4.4/lib \ /usr/prog/libsodium/lib /usr/prog/opencv-4.2/lib \ /usr/prog/nim/lib /usr/prog/libzip-1.10.1/lib; do - [ -d "$d" ] && LD_LIBRARY_PATH="$d:$LD_LIBRARY_PATH" + [ -d "$libdir" ] \ + && LD_LIBRARY_PATH="$libdir:$LD_LIBRARY_PATH" done export PATH LD_LIBRARY_PATH - $MRPY $MODDIR/moonraker-preflight.py $MODDIR /usr/data/config/moonraker.conf - ) > /tmp/mr-import.log 2>&1 || MRIMP=1 + $MOONRAKER_PYTHON $MODDIR/moonraker-preflight.py $MODDIR /usr/data/config/moonraker.conf + ) > /tmp/mr-import.log 2>&1 || MOONRAKER_PREFLIGHT_FAILED=1 sed 's/^/ /' /tmp/mr-import.log 2>/dev/null | tail -12 fi - if [ "$MRIMP" != 0 ]; then + if [ "$MOONRAKER_PREFLIGHT_FAILED" != 0 ]; then echo "!! moonraker: the shipped tree does not load on this printer -- keeping the stock one" echo " (nothing was moved; the web UI is unaffected)" - elif [ "${FREE:-0}" -lt "$NEED" ]; then - echo "!! moonraker: only ${FREE}KB free on /usr/prog, need ${NEED}KB -- keeping the stock tree" + elif [ "${FREE_KB:-0}" -lt "$NEED_KB" ]; then + echo "!! moonraker: only ${FREE_KB}KB free on /usr/prog, need ${NEED_KB}KB -- keeping the stock tree" else - rm -rf $MRROOT/moonraker.modold - if mv $MRROOT/moonraker $MRROOT/moonraker.modold; then - if cp -a $MODDIR/moonraker $MRROOT/moonraker; then + rm -rf $MOONRAKER_ROOT/moonraker.modold + if mv $MOONRAKER_ROOT/moonraker $MOONRAKER_ROOT/moonraker.modold; then + if cp -a $MODDIR/moonraker $MOONRAKER_ROOT/moonraker; then sync # Bytecode from the FlashForge build would otherwise be # the first thing imported. Same trap as klippy below. - find $MRROOT/moonraker -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null - rm -rf $MRROOT/moonraker.modold + find $MOONRAKER_ROOT/moonraker -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null + rm -rf $MOONRAKER_ROOT/moonraker.modold echo "moonraker: replaced with the mod's build" else - rm -rf $MRROOT/moonraker - mv $MRROOT/moonraker.modold $MRROOT/moonraker + rm -rf $MOONRAKER_ROOT/moonraker + mv $MOONRAKER_ROOT/moonraker.modold $MOONRAKER_ROOT/moonraker echo "!! moonraker: copy failed -- rolled back to the stock tree" fi else @@ -122,7 +124,7 @@ if [ -d $MODDIR/moonraker ]; then fi fi else - echo "!! moonraker: no $MRROOT/moonraker on this printer -- nothing replaced" + echo "!! moonraker: no $MOONRAKER_ROOT/moonraker on this printer -- nothing replaced" fi sync fi @@ -168,31 +170,31 @@ fi # has touched it and it is ours to replace. See the branch below. if [ -d $MODDIR/config ]; then mkdir -p /usr/data/config - for f in $MODDIR/config/*; do - [ -f "$f" ] || continue - b=`basename "$f"` - live="/usr/data/config/$b" - prev="$MODDIR/config-installed/$b" - case "$b" in + for source in $MODDIR/config/*; do + [ -f "$source" ] || continue + name=`basename "$source"` + live="/usr/data/config/$name" + prev="$MODDIR/config-installed/$name" + case "$name" in moonraker-custom.conf) # Yours, permanently. Created once so moonraker.conf's [include] # resolves -- Moonraker treats an include matching no file as a # fatal error -- and never touched again, not even as .mod-new. if [ -f "$live" ]; then - echo "config: $b kept (yours; never overwritten)" + echo "config: $name kept (yours; never overwritten)" else - cp -f "$f" "$live" - echo "config: $b created -- put your Moonraker settings here" + cp -f "$source" "$live" + echo "config: $name created -- put your Moonraker settings here" fi continue ;; ff-*.cfg) # Ours. Overwrite and say so when it had drifted, so the log # explains where a local edit went. - if [ -f "$live" ] && [ "`md5sum < "$live"`" != "`md5sum < "$f"`" ]; then - echo "config: $b overwritten (mod-owned; override it from printer.cfg)" + if [ -f "$live" ] && [ "`md5sum < "$live"`" != "`md5sum < "$source"`" ]; then + echo "config: $name overwritten (mod-owned; override it from printer.cfg)" fi - cp -f "$f" "$live" + cp -f "$source" "$live" continue ;; esac @@ -203,19 +205,19 @@ if [ -d $MODDIR/config ]; then # of OURS, and the shipped config could never reach the printer at all. # runConfig/ holds the pristine template the machine was built with; # matching it byte for byte means nobody has touched the live copy. - stock="/usr/prog/klipper/runConfig/$b" + stock="/usr/prog/klipper/runConfig/$name" if [ ! -f "$live" ]; then - cp -f "$f" "$live" + cp -f "$source" "$live" elif [ -f "$prev" ] && [ "`md5sum < "$live"`" = "`md5sum < "$prev"`" ]; then - cp -f "$f" "$live" - echo "config: $b updated (was unmodified)" + cp -f "$source" "$live" + echo "config: $name updated (was unmodified)" elif [ ! -f "$prev" ] && [ -f "$stock" ] \ && [ "`md5sum < "$live"`" = "`md5sum < "$stock"`" ]; then - cp -f "$f" "$live" - echo "config: $b installed over FlashForge's untouched copy" + cp -f "$source" "$live" + echo "config: $name installed over FlashForge's untouched copy" else - cp -f "$f" "$live.mod-new" - echo "config: $b kept -- new version left as $b.mod-new" + cp -f "$source" "$live.mod-new" + echo "config: $name kept -- new version left as $name.mod-new" fi done # Snapshot what we just shipped, for the NEXT update to compare against. @@ -251,19 +253,19 @@ sync # unchanged. The stick only ever sees a password once, on the first install. PW_KEEP="" if [ "$MOD_PW_AUTO" = "1" ] && [ -f $MODDIR/.prev-root-hash ]; then - PREV=`cat $MODDIR/.prev-root-hash 2>/dev/null` + PREV_HASH=`cat $MODDIR/.prev-root-hash 2>/dev/null` # Compare against the hash the PACKAGE ships ($WORK_DIR is the stock # run.sh's own variable, still in scope here), not the live file: the # stock installer has already copied the shipped shadow over the live one # by now, and if that copy ever fails the live file still holds the old # password -- which must read as "set by someone", not as "fresh". - SHIP="" + SHIPPED_HASH="" [ -n "${WORK_DIR:-}" ] && [ -f "$WORK_DIR/shadow" ] && - SHIP=`awk 'BEGIN{FS=":"} $1=="root"{print $2}' "$WORK_DIR/shadow" 2>/dev/null` - [ -n "$SHIP" ] || - SHIP=`awk 'BEGIN{FS=":"} $1=="root"{print $2}' /usr/prog/etc/shadow 2>/dev/null` - case "$PREV" in - '$'*) [ "$PREV" != "$SHIP" ] && PW_KEEP="$PREV" ;; + SHIPPED_HASH=`awk 'BEGIN{FS=":"} $1=="root"{print $2}' "$WORK_DIR/shadow" 2>/dev/null` + [ -n "$SHIPPED_HASH" ] || + SHIPPED_HASH=`awk 'BEGIN{FS=":"} $1=="root"{print $2}' /usr/prog/etc/shadow 2>/dev/null` + case "$PREV_HASH" in + '$'*) [ "$PREV_HASH" != "$SHIPPED_HASH" ] && PW_KEEP="$PREV_HASH" ;; esac fi rm -f $MODDIR/.prev-root-hash @@ -278,16 +280,16 @@ if [ -n "$PW_KEEP" ]; then echo "!! root password is the stock one -- no ssh login" fi elif [ "$MOD_PW_AUTO" = "1" ]; then - PW=`tr -dc A-Za-z0-9 < /dev/urandom 2>/dev/null | head -c 14` - H="" - [ -n "$PW" ] && H=`mkpasswd -m sha512 "$PW" 2>/dev/null` - case "$H" in + NEW_PASSWORD=`tr -dc A-Za-z0-9 < /dev/urandom 2>/dev/null | head -c 14` + NEW_HASH="" + [ -n "$NEW_PASSWORD" ] && NEW_HASH=`mkpasswd -m sha512 "$NEW_PASSWORD" 2>/dev/null` + case "$NEW_HASH" in '$6$'*) PWFILE=/mnt/anvil-password.txt { echo "anvil -- the root password for this printer" echo echo " ssh root@" - echo " password: $PW" + echo " password: $NEW_PASSWORD" echo echo "Save it somewhere safe and delete this file." echo "To change it, run passwd on the printer." @@ -295,10 +297,10 @@ elif [ "$MOD_PW_AUTO" = "1" ]; then sync # Read it back off the stick: proves the write survived, not just that # the shell accepted the redirect. - if grep -q "password: $PW" $PWFILE 2>/dev/null; then + if grep -q "password: $NEW_PASSWORD" $PWFILE 2>/dev/null; then # /etc is a bind mount of /usr/prog/etc, so this IS the live file # dropbear authenticates against. - awk -v h="$H" 'BEGIN{FS=OFS=":"} $1=="root"{$2=h} {print}' \ + awk -v h="$NEW_HASH" 'BEGIN{FS=OFS=":"} $1=="root"{$2=h} {print}' \ /usr/prog/etc/shadow > /usr/prog/etc/shadow.new && mv -f /usr/prog/etc/shadow.new /usr/prog/etc/shadow chmod 600 /usr/prog/etc/shadow diff --git a/payload/run-pre.sh b/payload/run-pre.sh index f62e2d8..1b88f1e 100644 --- a/payload/run-pre.sh +++ b/payload/run-pre.sh @@ -14,10 +14,11 @@ mkdir -p "$MODDIR" "$BACKUP" echo "=== mod install $STAMP ===" # app_startup.sh is deliberately NOT in this list: the mod replaces # firmwareExe instead, so the stock boot scripts are never modified. - for f in /usr/prog/klipper/start.sh /usr/prog/etc/passwd /usr/prog/etc/shadow; do - if [ -e "$f" ]; then - d="$BACKUP`dirname $f`" - mkdir -p "$d" && cp -a "$f" "$d/" && echo "backed up $f" + for file in /usr/prog/klipper/start.sh /usr/prog/etc/passwd \ + /usr/prog/etc/shadow; do + if [ -e "$file" ]; then + dest="$BACKUP`dirname $file`" + mkdir -p "$dest" && cp -a "$file" "$dest/" && echo "backed up $file" fi done # NOTE: we deliberately do NOT try to back up the previous firmwareExe here. diff --git a/payload/start.sh b/payload/start.sh index e352977..191da1c 100755 --- a/payload/start.sh +++ b/payload/start.sh @@ -54,8 +54,8 @@ fi # is the one step Klipper cannot start without, so do not depend on a lookup. # LD_LIBRARY_PATH still matters: this interpreter does not start without it, # which is exactly how moonrakerDaemon used to fail. -FF_PY=/usr/prog/Python-3.8.2/bin/python3 -[ -x "$FF_PY" ] || FF_PY=python3 +FF_PYTHON=/usr/prog/Python-3.8.2/bin/python3 +[ -x "$FF_PYTHON" ] || FF_PYTHON=python3 # # Test -f, not -x. The script is handed to the interpreter by path, so its # execute bit is irrelevant -- and it is not always set: the payload carries @@ -71,7 +71,7 @@ FF_PY=/usr/prog/Python-3.8.2/bin/python3 if [ "${FF_SKIP_MCU_BRINGUP:-0}" = 1 ]; then echo "start.sh: MCU bring-up already done by the caller" elif [ -f /usr/data/anvil/bin/ff_mcu_bringup.py ]; then - "$FF_PY" /usr/data/anvil/bin/ff_mcu_bringup.py \ + "$FF_PYTHON" /usr/data/anvil/bin/ff_mcu_bringup.py \ || echo "start.sh: MCU bring-up reported a problem ($?)" else echo "start.sh: ff_mcu_bringup.py missing -- the toolhead boards are not brought up" diff --git a/test/ffcfg.py b/test/ffcfg.py index 8b5aafb..8287780 100644 --- a/test/ffcfg.py +++ b/test/ffcfg.py @@ -16,26 +16,26 @@ OPTION = re.compile(r"^([^:=\s][^:=]*)\s*[:=]\s*(.*)$") def sections(path): """Yield (section_name, {option: value}) the way Klipper's parser sees it.""" - cur, opts = None, {} + section, options = None, {} key = None for raw in open(path, encoding="utf-8", errors="replace"): line = raw.rstrip("\n") if not line.strip() or line.lstrip().startswith("#"): continue - m = SECTION.match(line.strip()) - if m: - if cur: - yield cur, opts - cur, opts, key = m.group(1), {}, None + header = SECTION.match(line.strip()) + if header: + if section: + yield section, options + section, options, key = header.group(1), {}, None continue - if cur is None: + if section is None: continue if line[:1] in " \t" and key: # continuation line - opts[key] += "\n" + line + options[key] += "\n" + line continue - m = OPTION.match(line) - if m: - key = m.group(1).strip() - opts[key] = m.group(2) - if cur: - yield cur, opts + option = OPTION.match(line) + if option: + key = option.group(1).strip() + options[key] = option.group(2) + if section: + yield section, options diff --git a/test/ffsim/config.py b/test/ffsim/config.py index 8c58f67..101dfaa 100644 --- a/test/ffsim/config.py +++ b/test/ffsim/config.py @@ -44,9 +44,9 @@ set -a if [ -f "$1" ]; then . "$1" || exit 91; fi if [ -f "$2" ]; then . "$2" || exit 92; fi set +a -for k in %s; do - eval "v=\${$k-}" - printf '%%s=%%s\0' "$k" "$v" +for key in %s; do + eval "value=\${$key-}" + printf '%%s=%%s\0' "$key" "$value" done ''' % " ".join(WANTED) @@ -66,31 +66,31 @@ class Config: config_env = os.environ.get("CONFIG_ENV") or str(root / "config.env") test_env = os.environ.get("TEST_ENV") or str(root / "test.env") - proc = subprocess.run( + dumped = subprocess.run( ["bash", "-c", _DUMP, "ffsim", config_env, test_env], cwd=str(root), capture_output=True, text=True) - if proc.returncode != 0: + if dumped.returncode != 0: # A syntax error in config.env is a BROKEN harness, not a missing # precondition. The shell version could not tell the two apart: # the half-sourced file left STOCK_TGZ_* empty, the launcher # concluded there was nothing to test, and it reported a clean # skip. Naming the file that failed is the entire point. - which = {91: config_env, 92: test_env}.get(proc.returncode) + which = {91: config_env, 92: test_env}.get(dumped.returncode) raise Fail("could not read %s:\n%s" % (which or ("%s / %s" % (config_env, test_env)), - proc.stderr.strip())) + dumped.stderr.strip())) values = {} - for item in proc.stdout.split("\0"): + for item in dumped.stdout.split("\0"): if "=" in item: - k, _, v = item.partition("=") - values[k] = v + key, _, value = item.partition("=") + values[key] = value # The environment wins -- see the module docstring. Being SET is what # counts, so this tests membership rather than truthiness. - for k in OVERRIDABLE: - if k in os.environ: - values[k] = os.environ[k] + for key in OVERRIDABLE: + if key in os.environ: + values[key] = os.environ[key] return cls(values, root, config_env, test_env) diff --git a/test/ffsim/gates.py b/test/ffsim/gates.py index e6733af..64b9c3f 100644 --- a/test/ffsim/gates.py +++ b/test/ffsim/gates.py @@ -45,7 +45,7 @@ def extract_rootfs(config, on_output=None): kernels = sorted(outer.glob("kernel-*.tar.xz")) if not kernels: raise Fail("no kernel-*.tar.xz in the package (a --slim build has none)") - kern = kernels[0] + kernel_tarball = kernels[0] kerndir = root / "work" / "kern" rootfs = root / "work" / "rootfs" @@ -54,7 +54,7 @@ def extract_rootfs(config, on_output=None): _rmtree(d) kerndir.mkdir(parents=True) - with tarfile.open(str(kern)) as tf: + with tarfile.open(str(kernel_tarball)) as tf: _extract_all(tf, kerndir) squash = None @@ -62,15 +62,15 @@ def extract_rootfs(config, on_output=None): squash = path break if squash is None: - raise Fail("no rootfs.squashfs inside %s" % kern.name) + raise Fail("no rootfs.squashfs inside %s" % kernel_tarball.name) if on_output: on_output(">> %s" % squash.name) - proc = subprocess.run( + completed = subprocess.run( ["unsquashfs", "-q", "-d", str(rootfs), str(squash)], capture_output=True, text=True) - if proc.returncode != 0: - raise Fail("unsquashfs failed:\n%s" % proc.stderr.strip()) + if completed.returncode != 0: + raise Fail("unsquashfs failed:\n%s" % completed.stderr.strip()) _rmtree(kerndir) if not (rootfs / "bin").is_dir(): diff --git a/test/ffsim/replica.py b/test/ffsim/replica.py index f37c2dd..8fff36d 100644 --- a/test/ffsim/replica.py +++ b/test/ffsim/replica.py @@ -79,12 +79,12 @@ class Replica: "in test.env to skip it.") image = "creator5-printer-sim" - ctx = config.root / "test" / "integration" / "printer" + build_dir = config.root / "test" / "integration" / "printer" # Always rebuild: a cache hit takes about a second, and a stale image # silently testing yesterday's harness is not a trade worth making. built = subprocess.run( [docker, "build", "-q", "-t", image, - "-f", str(ctx / "Dockerfile"), str(ctx)], + "-f", str(build_dir / "Dockerfile"), str(build_dir)], capture_output=True, text=True) if built.returncode != 0: raise Fail("could not build %s:\n%s" % (image, built.stderr.strip())) @@ -101,7 +101,7 @@ class Replica: """ packages = packages or {} stage = stage or self.stage_dir() - cfg = self.config + config = self.config argv = [self.docker, "run", "--rm", "-i", "--privileged"] if not self.prebuilt: @@ -111,7 +111,7 @@ class Replica: "-v", "%s/pkgs:/pkgs:ro" % stage, "-v", "%s/case.sh:/case.sh:ro" % stage, "-v", "%s/payload:/payload:ro" % self.root, - "-e", "FF_KEY=%s" % cfg.ff_key, + "-e", "FF_KEY=%s" % config.ff_key, "-e", "BASE_PKG=%s" % ("/pkgs/base.tgz" if base_pkg else ""), "-e", "PKGS=%s" % "".join(" %s=/pkgs/%s" % (n, n) for n in packages), ] @@ -119,16 +119,16 @@ class Replica: # A real /usr/prog taken off a printer. Only for a locally built # replica: a prebuilt image already has one baked in, and mounting # over it would replace the genuine tree with an older copy. - dump = cfg.get("PROG_DUMP") - if not self.prebuilt and dump and os.path.exists(dump): - dump_abs = os.path.abspath(dump) + prog_dump = config.get("PROG_DUMP") + if not self.prebuilt and prog_dump and os.path.exists(prog_dump): + dump_abs = os.path.abspath(prog_dump) argv += ["-e", "PROG_DUMP=/progdump", "-v", "%s:/progdump:ro" % dump_abs] else: argv += ["-e", "PROG_DUMP="] argv += [ - "-e", "PROG_MB=%s" % cfg.get("PROG_MB"), - "-e", "DATA_MB=%s" % cfg.get("DATA_MB"), + "-e", "PROG_MB=%s" % config.get("PROG_MB"), + "-e", "DATA_MB=%s" % config.get("DATA_MB"), "-e", "SIM_VERBOSE=%s" % os.environ.get("SIM_VERBOSE", "0"), "-e", "USB_STICK=%s" % ("1" if usb_stick else "0"), self.image, "/case.sh", @@ -170,14 +170,14 @@ class Replica: shutil.copy(base_pkg, str(stage / "pkgs" / "base.tgz")) argv = self.command(case, packages, base_pkg, usb_stick, stage) - proc = subprocess.run(argv, capture_output=True, text=True) - body = (proc.stdout or "") + (proc.stderr or "") - if on_output and body.strip(): - on_output(body) - if proc.returncode != 0: + completed = subprocess.run(argv, capture_output=True, text=True) + output = (completed.stdout or "") + (completed.stderr or "") + if on_output and output.strip(): + on_output(output) + if completed.returncode != 0: raise Fail("%s exited %d" % (os.path.basename(case), - proc.returncode)) - return body + completed.returncode)) + return output finally: # The staged packages are ~80MB each. shutil.rmtree(str(stage), ignore_errors=True) diff --git a/test/integration/printer/case-moonraker.sh b/test/integration/printer/case-moonraker.sh index 2dc2d17..914f0bc 100755 --- a/test/integration/printer/case-moonraker.sh +++ b/test/integration/printer/case-moonraker.sh @@ -25,7 +25,7 @@ skip() { echo " SKIP $*"; } PY=/usr/prog/Python-3.8.2/bin/python3 MR=/usr/prog/moonraker/moonraker -MR_MAIN=/usr/prog/moonraker/moonraker/moonraker/moonraker.py +MOONRAKER_MAIN=/usr/prog/moonraker/moonraker/moonraker/moonraker.py S60=/tmp/payload/init.d/S60web [ -x "$PY" ] || { bad "no interpreter at $PY"; exit 1; } @@ -100,9 +100,9 @@ fi # 3. S60web now starts moonraker itself, so the entry point it names has to # be there and the tools it uses have to exist. A path that is wrong here # means no web UI at all, and the failure would be a silent one. -[ -f "$MR_MAIN" ] \ - && ok "the entry point S60web names is present ($MR_MAIN)" \ - || bad "no moonraker.py at $MR_MAIN" +[ -f "$MOONRAKER_MAIN" ] \ + && ok "the entry point S60web names is present ($MOONRAKER_MAIN)" \ + || bad "no moonraker.py at $MOONRAKER_MAIN" command -v start-stop-daemon >/dev/null 2>&1 \ && ok "start-stop-daemon is available for the pidfile" \ diff --git a/test/integration/test_moonraker_libpath.py b/test/integration/test_moonraker_libpath.py index 2e15b53..d1d8135 100644 --- a/test/integration/test_moonraker_libpath.py +++ b/test/integration/test_moonraker_libpath.py @@ -91,7 +91,7 @@ def test_the_two_things_the_daemon_did_are_kept(): # moonraker's temporary files -- uploads included -- off /tmp, which is a # ramdisk here. Dropping either was the reason not to do this before. text = code(S60WEB) - assert "start-stop-daemon -S" in text and "-p $MR_PID" in text + assert "start-stop-daemon -S" in text and "-p $MOONRAKER_PID" in text assert "start-stop-daemon -K" in text assert "TMPDIR" in text @@ -111,4 +111,4 @@ def test_a_dead_moonraker_is_reported_in_the_boot_log(): # opens a browser. The boot log is the only place that can say so. text = code(S60WEB) assert "moonraker_check" in text - assert "moonraker.log" in text or "MR_LOG" in text + assert "moonraker.log" in text or "MOONRAKER_LOG" in text diff --git a/test/run-tests.py b/test/run-tests.py index ab0a394..8ea3fcb 100755 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -54,29 +54,30 @@ def indented(reporter): # ------------------------------------------------------------ static checks -def check_shell_syntax(r): +def check_shell_syntax(reporter): """One line per check, not one per file: 25 green lines saying "syntax ok" hide the two that matter.""" targets = [] for pattern in ("bin/*.sh", "payload/*.sh", "payload/init.d/S*", "payload/firmwareExe", "test/integration/printer/*.sh"): targets += sorted(ROOT.glob(pattern)) - bad = [] - for f in targets: - if not f.is_file(): + broken = [] + for script in targets: + if not script.is_file(): continue - proc = subprocess.run(["bash", "-n", str(f)], - capture_output=True, text=True) - if proc.returncode != 0: - bad.append(f.relative_to(ROOT)) - r._say(" %s\n" % proc.stderr.strip()) - if bad: - raise Fail("syntax errors in: %s" % " ".join(str(b) for b in bad)) + parsed = subprocess.run(["bash", "-n", str(script)], + capture_output=True, text=True) + if parsed.returncode != 0: + broken.append(script.relative_to(ROOT)) + reporter._say(" %s\n" % parsed.stderr.strip()) + if broken: + raise Fail("syntax errors in: %s" + % " ".join(str(path) for path in broken)) if not targets: raise Fail("no scripts found to parse -- the globs are wrong") -def check_no_bashisms(r): +def check_no_bashisms(reporter): """The payload runs on the printer's busybox ash, which is not bash. shellcheck's dash dialect knows every "not supported in POSIX sh" @@ -96,24 +97,26 @@ def check_no_bashisms(r): # ash-compatibility check silently. if not targets: raise Fail("no payload scripts found to check -- has payload/ moved?") - proc = subprocess.run(["shellcheck", "-s", "dash", "-f", "gcc"] + targets, - capture_output=True, text=True) - hits = [ln for ln in (proc.stdout or "").split("\n") - if "SC3" in ln or "SC2039" in ln] + checked = subprocess.run( + ["shellcheck", "-s", "dash", "-f", "gcc"] + targets, + capture_output=True, text=True) + hits = [line for line in (checked.stdout or "").split("\n") + if "SC3" in line or "SC2039" in line] # shellcheck exits 1 for findings (which `hits` covers) but also for a # usage or file error, which would otherwise pass unnoticed. - if not hits and proc.returncode not in (0, 1): + if not hits and checked.returncode not in (0, 1): raise Fail("shellcheck failed to run (exit %d): %s" - % (proc.returncode, (proc.stderr or "").strip()[:200])) + % (checked.returncode, + (checked.stderr or "").strip()[:200])) if hits: for ln in hits[:10]: - r._say(" %s\n" % ln) + reporter._say(" %s\n" % ln) raise Fail("bashisms in the payload") # ------------------------------------------------------------------- pytest -def run_pytest(r): +def run_pytest(reporter): """Counts come from the JUnit XML, not from reading the summary line. The shell version grepped stdout for "N skipped". That is the same @@ -121,15 +124,15 @@ def run_pytest(r): is just as wrong here even though pytest is well behaved about it. """ with tempfile.TemporaryDirectory() as tmp: - xml = os.path.join(tmp, "results.xml") - proc = subprocess.run( + results_xml = os.path.join(tmp, "results.xml") + completed = subprocess.run( [sys.executable, "-m", "pytest", "./test/integration", "-q", - "--junitxml=" + xml], + "--junitxml=" + results_xml], capture_output=True, text=True, cwd=str(ROOT)) - if not os.path.exists(xml): + if not os.path.exists(results_xml): raise Fail("pytest produced no results:\n%s" - % (proc.stdout or proc.stderr)[-2000:]) - suite = ET.parse(xml).getroot() + % (completed.stdout or completed.stderr)[-2000:]) + suite = ET.parse(results_xml).getroot() if suite.tag == "testsuites": suite = suite.find("testsuite") total = int(suite.get("tests", 0)) @@ -156,9 +159,9 @@ def run_pytest(r): # was written to end (see test-abi.sh in docs/testing.md). if not total: raise Fail("pytest collected no tests (exit %d) -- collection is " - "broken, or the suite has moved" % proc.returncode) + "broken, or the suite has moved" % completed.returncode) if failures: - r._say((proc.stdout or "")[-4000:]) + reporter._say((completed.stdout or "")[-4000:]) raise Fail("pytest: %d failed, %d passed, %d skipped" % (failures, passed, skipped)) if skipped: @@ -168,16 +171,16 @@ def run_pytest(r): # "pytest skipped" can be checked against what SHOULD be unrunnable # here rather than taken on trust. for line in names: - r._say(" %s\n" % line) + reporter._say(" %s\n" % line) raise Skip("%d passed, %d skipped: %s" % (passed, skipped, "; ".join(n.split(" -- ")[0] for n in names))) - r._say(" %d passed\n" % passed) + reporter._say(" %d passed\n" % passed) # ------------------------------------------- packaging, on a synthetic stock -def make_fixture(r, tmp): +def make_fixture(reporter, tmp): """Build the synthetic stock package and the throwaway config for it. Returns the environment the build steps should run under. Each of those @@ -188,11 +191,12 @@ def make_fixture(r, tmp): # Inside the repo: the replica starts sibling containers through the # docker socket, and those mounts are resolved by the host daemon, where # a path under this container's /tmp does not exist. - r.run(["./test/integration/make-stock-fixture.sh", str(fxdir)], cwd=ROOT) + reporter.run(["./test/integration/make-stock-fixture.sh", str(fxdir)], + cwd=ROOT) - fixture = fxdir / "Creator5Pro-stock-fixture.tgz" - if not fixture.is_file(): - raise Fail("make-stock-fixture.sh produced no %s" % fixture.name) + stock_package = fxdir / "Creator5Pro-stock-fixture.tgz" + if not stock_package.is_file(): + raise Fail("make-stock-fixture.sh produced no %s" % stock_package.name) # Stand-ins for Mainsail and HelixScreen. The real ones are a 3MB zip and # a 60MB tarball; the tests must not need the network, but they DO need @@ -218,24 +222,25 @@ def make_fixture(r, tmp): # verify gate then reads is a stump: no init.d, no HelixScreen, no # Moonraker. That is exactly how it failed in CI while `make test` was # green locally against a populated vendor/. - mr = assets / "mr" / "moonraker-fixture" / "moonraker" - (mr / "components").mkdir(parents=True, exist_ok=True) - (mr / "moonraker.py").write_text("# moonraker fixture\n") - (mr / "components" / "webcam.py").write_text('# "enabled"\n') + moonraker_fixture = assets / "mr" / "moonraker-fixture" / "moonraker" + (moonraker_fixture / "components").mkdir(parents=True, exist_ok=True) + (moonraker_fixture / "moonraker.py").write_text("# moonraker fixture\n") + (moonraker_fixture / "components" / "webcam.py").write_text( + '# "enabled"\n') subprocess.run(["tar", "-czf", str(assets / "moonraker.tar.gz"), "-C", str(assets / "mr"), "moonraker-fixture"], check=True) # A throwaway config, passed through CONFIG_ENV. It used to overwrite # ./config.env and copy it back afterwards, which put the config you # edited one crashed run away from being replaced by a fixture one. - cfg = Path(tmp) / "config.env" + config_file = Path(tmp) / "config.env" # BUILD_KLIPPER=stock, BY NAME: this job must not need the network, and # the fork path needs the pinned tarball plus the ~203MB toolchain from # vendor/. patch.sh no longer falls back silently -- KLIPPER_FORK="" here # used to mean "quietly keep the stock tree", which is exactly how # v20260824 shipped without its Klipper. The fork path is exercised where # vendor/ exists: the printer-sim job and the release workflow. - cfg.write_text( + config_file.write_text( "MOD_NAME=anvil\n" "MOD_VER=ci\n" 'SW_VER=""\n' @@ -248,26 +253,28 @@ def make_fixture(r, tmp): "TARGET_MACHINE=Creator5Pro\n" "ROOT_PW_HASH='$6$ci$abcdefghijklmnopqrstuvwxyz'\n" "FF_KEY='FFP0331&*%%root'\n" - % (fixture, assets / "helixscreen.tar.gz", assets / "mainsail.zip", + % (stock_package, assets / "helixscreen.tar.gz", + assets / "mainsail.zip", assets / "moonraker.tar.gz")) - return dict(os.environ, CONFIG_ENV=str(cfg), TARGET_MACHINE="Creator5Pro") + return dict(os.environ, CONFIG_ENV=str(config_file), + TARGET_MACHINE="Creator5Pro") # --------------------------------------------------------------------- main def main(): - r = Reporter() - emit = indented(r) + reporter = Reporter() + emit = indented(reporter) config = Config.load(ROOT) - r.hdr("shell syntax") - with r.gate("every script parses"): - check_shell_syntax(r) + reporter.hdr("shell syntax") + with reporter.gate("every script parses"): + check_shell_syntax(reporter) - r.hdr("no bashisms in the on-printer payload") - with r.gate("no bash-only constructs in the payload"): - check_no_bashisms(r) + reporter.hdr("no bashisms in the on-printer payload") + with reporter.gate("no bash-only constructs in the payload"): + check_no_bashisms(reporter) stock = config.stock_for() @@ -276,30 +283,30 @@ def main(): # both the config gate and the rootfs checks. This is also the unpack the # replica gates need later. if stock: - r.hdr("extracting the printer rootfs") + reporter.hdr("extracting the printer rootfs") if (ROOT / "work" / "rootfs" / "bin").is_dir(): - r.ok("rootfs already extracted") + reporter.ok("rootfs already extracted") else: - with r.gate("extract the printer rootfs"): - r.run(["./bin/unpack.sh"], cwd=ROOT) + with reporter.gate("extract the printer rootfs"): + reporter.run(["./bin/unpack.sh"], cwd=ROOT) gates.extract_rootfs(config, on_output=emit) - r.hdr("python checks") - with r.gate("pytest"): - run_pytest(r) + reporter.hdr("python checks") + with reporter.gate("pytest"): + run_pytest(reporter) with tempfile.TemporaryDirectory() as tmp: - r.hdr("packaging, on a synthetic stock package") + reporter.hdr("packaging, on a synthetic stock package") env = None - with r.gate("synthetic stock package"): - env = make_fixture(r, tmp) + with reporter.gate("synthetic stock package"): + env = make_fixture(reporter, tmp) - r.hdr("build on the fixture") + reporter.hdr("build on the fixture") if env is None: # The fixture is the input to every step below. Without it they # would each fail for the same reason, which reads as four # separate problems instead of one. - with r.gate("build on the fixture"): + with reporter.gate("build on the fixture"): raise Fail("no fixture -- the steps below cannot run") else: # Stop at the first step that fails. These are a chain, not four @@ -313,20 +320,22 @@ def main(): # Counted from here, not from zero: an earlier gate (pytest, say) # may already have failed, and that is not a reason to skip the # build. - failures_before = r.failed + failures_before = reporter.failed broken = False for step in ("unpack", "patch", "pack"): - with r.gate(step): - r.run(["./bin/%s.sh" % step], cwd=ROOT, env=env) - if r.failed > failures_before: + with reporter.gate(step): + reporter.run(["./bin/%s.sh" % step], cwd=ROOT, env=env) + if reporter.failed > failures_before: broken = True break if not broken: - with r.gate("verify"): - pkgs = sorted((ROOT / "work" / "out").glob("Creator5Pro-*.tgz")) - if not pkgs: + with reporter.gate("verify"): + packages = sorted( + (ROOT / "work" / "out").glob("Creator5Pro-*.tgz")) + if not packages: raise Fail("no package produced") - r.run(["./bin/verify.sh", str(pkgs[0])], cwd=ROOT, env=env) + reporter.run(["./bin/verify.sh", str(packages[0])], + cwd=ROOT, env=env) # Throw away everything the fixture half built. bin/ hardcodes work/, so # those packages land in the same work/out a real build uses -- and a @@ -336,38 +345,38 @@ def main(): shutil.rmtree(str(ROOT / "work" / d), ignore_errors=True) if not stock: - r.hdr("printer replica") - with r.gate("the printer replica"): + reporter.hdr("printer replica") + with reporter.gate("the printer replica"): raise Skip("no stock package in config.env -- these are the gates " "that decide whether a package bricks a printer") else: - r.hdr("MCU bring-up runs on the printer's own Python") - with r.gate("mcu bring-up"): + reporter.hdr("MCU bring-up runs on the printer's own Python") + with reporter.gate("mcu bring-up"): gates.mcu_bringup(config, on_output=emit) - with r.gate("boot screen"): + with reporter.gate("boot screen"): gates.boot_screen(config, on_output=emit) - with r.gate("moonraker"): + with reporter.gate("moonraker"): gates.moonraker(config, on_output=emit) - r.hdr("end-to-end update on the printer replica") - with r.gate("boot -> install -> re-install -> boot"): + reporter.hdr("end-to-end update on the printer replica") + with reporter.gate("boot -> install -> re-install -> boot"): for step in ("unpack", "patch", "pack"): - r.run(["./bin/%s.sh" % step], cwd=ROOT) - pkgs = sorted((ROOT / "work" / "out").glob("*-*.tgz")) - if not pkgs: + reporter.run(["./bin/%s.sh" % step], cwd=ROOT) + packages = sorted((ROOT / "work" / "out").glob("*-*.tgz")) + if not packages: raise Fail("no package produced") - gates.install(config, str(pkgs[0]), on_output=emit) + gates.install(config, str(packages[0]), on_output=emit) - r.hdr("recovery: a stock package reverts the mod") - with r.gate("install mod -> flash stock -> back to stock"): - pkgs = sorted((ROOT / "work" / "out").glob("*-*.tgz")) - if not pkgs: + reporter.hdr("recovery: a stock package reverts the mod") + with reporter.gate("install mod -> flash stock -> back to stock"): + packages = sorted((ROOT / "work" / "out").glob("*-*.tgz")) + if not packages: raise Fail("no package produced") - gates.roundtrip(config, str(pkgs[0]), stock, on_output=emit) + gates.roundtrip(config, str(packages[0]), stock, on_output=emit) - return r.summary() + return reporter.summary() if __name__ == "__main__":