Files
firmware/bin/payload.sh
Oleksandr ShyshatskyiandClaude Opus 5 bf7d0f7e06 The printer has no clock and nothing was setting it
There is no RTC and no battery, so every boot starts at whatever the
kernel was built with and stays there: job timestamps, log ordering and
certificate validity are all wrong on a machine that has been unplugged.
Nothing on the stock rootfs could fix it -- FlashForge's busybox is
v1.31.1 built without the ntpd applet, and its rdate speaks RFC 868.

anvil-sntpd is troglobit's sntpd, a fork of Doolittle's ntpclient: one
small binary, servers and interval on the command line, so there is no
config file, no privsep user and no chroot to place on a read-only
rootfs. It ships $MODDIR/sbin/sntpd and nothing else.

A LONGRUN, NOT A HOOK ON THE WIFI ONESHOT. That oneshot runs once, at
boot, and returns before wlan0 has associated -- so a sync hung off its
end would fire too early, never retry, and never fire at all for the
owner who joins a network from the HelixScreen panel an hour later. And
with no RTC a clock is not set once but maintained. sntpd keeps its
schedule with the network down instead of exiting, which is what lets
this service need no wifi dependency -- just as well, since nothing may
depend on that oneshot.

The poll interval is 300 rather than upstream's 600 because it is also
the ceiling on the down-network retry backoff, and so bounds how long a
printer that joined wifi late keeps the wrong time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149y4iyLpEwYKh2PS4fmf4u
2026-08-30 16:03:55 +03:00

106 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# 2/3 -- build the mod payload from the package feed. Idempotent.
#
# Install the feed with the printer's own opkg, seed the user's
# moonraker-custom.conf, write the manifest. Everything it produces goes to
# $MODDIR on the data partition and nothing to /usr/prog, so it needs the
# package feed and no stock FlashForge package at all.
set -euo pipefail
. "$(dirname "$0")/common.sh"
. "$ROOT/pkgs/lib.sh"
# Counting .ipk, not testing the directory: a cleaned tree leaves it empty.
if [ -z "$(ls "$PKG_FEED"/*.ipk 2>/dev/null)" ]; then
echo "no package feed at $PKG_FEED" >&2
echo " the recipes this script runs build against it -- run 'make packages' first." >&2
exit 1
fi
say() { printf '>> %s\n' "$*"; }
skip() { printf ' (skip) %s\n' "$*"; }
# Everything goes to /usr/data: /usr/prog is the firmware partition and would
# overflow on ~100MB of Mainsail and HelixScreen.
rm -rf "$PAYLOAD_ROOT"
# --- 1. the payload, installed
# Installed by the printer's own opkg inside the replica, so `opkg
# list-installed` answers what a release installs off the payload itself.
say "payload: installing the feed with the printer's own opkg"
# Not model-specific; TARGET_MACHINE names only the OUTPUT file, because
# runFirmwareExe.sh refuses a package whose machine is not its own.
# The roots, not the closure: Depends brings the rest, so the metadata is
# exercised on every build. A root with no .ipk is skipped -- that is how the
# PKG_WHEN gates reach here -- but a missing DEPENDENCY is still opkg's error.
# The two loose python packages are Recommends, which opkg has no field for.
#
# greenlet and cffi are deliberately NOT here: the klipper service execs
# $FF_PYTHON, so they are ordinary Depends of anvil-klipper now. Listed here
# they were installed by every build and by no `opkg install anvil-klipper`,
# which is the one command that has to work on a printer.
MOD_ROOTS="anvil-core anvil-opkg anvil-s6-rc anvil-klipper
anvil-moonraker anvil-python-pillow anvil-python-preprocess-cancellation
anvil-mainsail anvil-helixscreen anvil-busybox anvil-sntpd"
# Named, not versioned -- except anvil-core, whose PKG_VERSION is MOD_VER, so a
# feed built yesterday would install yesterday's anvil-core.
[ -f "$(pkg_ipk anvil-core)" ] || pkg_die \
"the feed has no $(basename "$(pkg_ipk anvil-core)") -- rerun ./bin/build-packages.sh"
MOD_INSTALL=""
for _p in $MOD_ROOTS; do
for _f in "$PKG_FEED/${_p}_"*.ipk; do
if [ -f "$_f" ]; then MOD_INSTALL="$MOD_INSTALL $_p"; fi
break
done
done
# The machine's own opkg installs the feed onto its own filesystem: needs a
# privileged container, hence `make build`'s docker lane.
# shellcheck disable=SC2086
./bin/build-payload.py $MOD_INSTALL
say "payload: $(grep -c '^Package:' "$PAYLOAD_DIR/var/lib/opkg/status") packages installed (both chamber configs; the printer picks)"
# --- 2. Klipper
# Nothing to do, and nothing to assert. anvil-klipper installs the whole klippy
# tree at $MODDIR/klipper/klippy and the klipper s6-rc service execs it there on
# our own $FF_PYTHON, so the package IS the printer's Klipper. /usr/prog/klipper
# keeps FlashForge's stock klippy, unread -- the only file still read there is
# klipper_pri.sh, their SCHED_FIFO helper. klipperDaemon at that path is a
# symlink to our shim, pointed there by anvil-link-prog.sh.
#
# A gate here would ask whether opkg had installed the package it was just
# handed, of a staging root three lines after it was filled.
# qa/replica/test_install.py::test_klippy_is_present asks it of an installed
# filesystem instead, which is the vehicle that reaches a printer.
# --- 3. the user's seams
# In the payload and in NO package: a package member is overwritten on every
# upgrade by definition, and this is the user's own file.
[ -f pkgs/moonraker/seed/moonraker-custom.conf ] \
&& cp -f pkgs/moonraker/seed/moonraker-custom.conf "$PAYLOAD_DIR/config/moonraker-custom.conf"
# moonraker.conf ships with pkgs/moonraker, not anvil-core: unconditional, a
# BUILD_MOONRAKER=0 build overwrote the config of a server it did not install.
# --- 4. the s6-rc database
# Compiled in the replica by case-build-payload.sh, with the s6-rc-compile
# anvil-s6-rc ships, straight after opkg installs the feed. Compiling with the
# compiler we SHIP is what makes the #! baked into the oneshot runner point at
# the execline we ship rather than the build host's -- a native second build of
# the same tarballs had to be held to --prefix=$MODDIR by hand to get that
# right. (Byte-identical either way: 80-s6-migration.md, measured 2026-08-28.)
#
# Nothing to assert here either. qa/replica/test_s6rc.py boots the database on
# a machine: s6-rc-init reads it, the boot set comes up, and a database moved
# aside is reported.
echo
echo "Payload built."
echo " mod payload: $(du -sh "$PAYLOAD_DIR" | cut -f1) (-> /usr/data/anvil, data partition)"
echo " nothing goes to /usr/prog: no software component is shipped."
echo "Now run ./bin/pack.sh"