mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 21:02:16 -05:00
Two changes that turned out to be one.
MOVED, NOT DELETED. `test/ffsim/` and `test/integration/printer/` are
`tools/replica/`. They were never tests: bin/patch.sh calls
bin/build-payload.py, which starts a replica and assembles the payload by
running the PRINTER'S OWN opkg against / inside it -- so the "test harness"
was on the build path, and deleting it as asked would have broken `make
build`. qa/lib/replica.py drives the same directory unmodified. No content
changed; what changed is qa/lib/replica.py's build directory, three launcher
sys.path lines, the Makefile targets and the CI shellcheck glob.
tools/replica/README.md says who drives it and why it is not under test/.
CUT TO 28. The five surviving modules were 136 tests; they are 28, chosen on
one question: would this failure be SILENT? What is kept is the mistakes that
run perfectly and are wrong --
test_startup 8 no stamp unless the values are verifiably saved (a
stamp written early is a printer that has lost its
factory calibration for good), and the handover order
test_tool_transform 7 every one is a move to the wrong place: a sign, a
frame left applied while probing, a stale gcode_move
cache, a config a calibrated printer cannot parse
test_ffscreen 5 the four ways it can do harm rather than look wrong
test_chamber 4 the chamber mistakes that cost a print before anyone
sees them, plus the parse gate they rest on
test_gcode 4 a renamed macro, and the safe file being cold
What went: the panel narration, the geometry probe and parse_geometry, the
z_adjust and per-job-Z bookkeeping, the config layout checks, the G-code
bounds checks. Each is either a wrong number shown to a person, or a failure
the printer reports loudly the first time -- and qa/replica/test_boot_screen.py
already renders the real frames on the real machine.
The rewrite is not a delete: assertions were folded so a kept test asks its
question in every direction it can be got wrong (both models, both rotations,
all four ways the migration can fail), and each file's header now says what it
dropped and why, where the next person will open it.
Also gone: conftest's `rootfs` fixture, orphaned two commits ago. Nothing under
test/ needs the firmware, docker, or a network any more.
28 host-side tests pass in 0.15s. qa/static unchanged at 218. `make rootfs`
re-verified end to end from its new path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzLebCrvc8ssWR5JuEsha4
92 lines
4.0 KiB
Bash
Executable File
92 lines
4.0 KiB
Bash
Executable File
#!/bin/sh
|
|
# Render every first-boot screen inside the replica and print each one as a
|
|
# base64 PNG on stdout.
|
|
#
|
|
# This is the answer to "I want to SEE it, not read assertions about it". The
|
|
# frames are drawn by $MODDIR/bin/python3.13 -- our own cross-built CPython,
|
|
# the interpreter FF_PYTHON names, running MIPS under qemu against the
|
|
# printer's real rootfs -- so what comes out is what the panel would show, not
|
|
# what a developer's interpreter thinks it would.
|
|
#
|
|
# stdout is the only way out: every mount the replica gets is read-only. The
|
|
# frames are PNG-compressed in here (zlib is stdlib) rather than shipped raw,
|
|
# because a 1024x600x32 frame is 2.4MB and there are several of them.
|
|
#
|
|
# tools/replica/sim-boot-screen.py drives this and writes the PNGs out.
|
|
MOD=/usr/data/anvil
|
|
PY=$MOD/bin/python3.13
|
|
mkdir -p $MOD/bin
|
|
gzip -dc /mnt/py.tgz | tar -x -C $MOD
|
|
cp /tmp/payload/bin/ffscreen.py $MOD/bin/ffscreen.py
|
|
"$PY" - <<'PYEOF'
|
|
import base64, struct, sys, zlib
|
|
sys.path.insert(0, '/usr/data/anvil/bin')
|
|
import ffscreen
|
|
|
|
W, H, BPP = 480, 800, 32 # the real framebuffer: portrait
|
|
# Keep in step with bin/preview-boot-screen.py, which renders the same list
|
|
# on the host -- the two are expected to agree byte for byte.
|
|
PHASES = [
|
|
('starting-services', 'STARTING SERVICES', 0.05, ''),
|
|
('mcu-boards', 'WAKING THE TOOLHEAD BOARDS', 0.05, ''),
|
|
('mcu-heater', 'WAKING THE HEATER BOARD', 0.08, ''),
|
|
('mcu-both', 'WAKING THE HEATER BOARD AND THE LEVEL BOARD', 0.08, ''),
|
|
('waiting', 'WAITING FOR THE PRINTER', 0.22, ''),
|
|
('importing', 'READING FACTORY CALIBRATION', 0.5, ''),
|
|
('saving', 'SAVING CALIBRATION', 0.7, ''),
|
|
('restarting', 'RESTARTING THE PRINTER', 0.85, ''),
|
|
('complete', 'SETUP COMPLETE', 1.0, ''),
|
|
('already', 'ALREADY CALIBRATED', 1.0, ''),
|
|
]
|
|
RETRY = 'SETUP WILL RETRY ON NEXT START'
|
|
LOG = '/USR/DATA/LOGS/ANVIL-BOOT.LOG'
|
|
for slug, reason in [
|
|
('fail-moonraker', 'MOONRAKER IS NOT RESPONDING'),
|
|
('fail-klipper-error', 'KLIPPER REPORTED AN ERROR'),
|
|
('fail-board', 'THE HEATER BOARD DID NOT ANSWER'),
|
|
('fail-nostart', 'KLIPPER COULD NOT BE STARTED'),
|
|
('fail-klipper-startup', 'KLIPPER DID NOT FINISH STARTING (STARTUP)'),
|
|
('fail-tools', 'COULD NOT READ THE TOOL SETTINGS'),
|
|
('fail-config', 'COULD NOT READ THE PRINTER CONFIGURATION'),
|
|
('fail-pending', 'ANOTHER CONFIGURATION SAVE WAS ALREADY WAITING'),
|
|
('fail-refused', 'THE PRINTER REFUSED FF_IMPORT_FIRMWARE_CONFIG'),
|
|
('fail-empty', 'NO CALIBRATION FOUND IN THE FACTORY DATA'),
|
|
('fail-restart', 'KLIPPER DID NOT RESTART AFTER SAVING'),
|
|
('fail-unsaved', 'THE CALIBRATION DID NOT SAVE')]:
|
|
PHASES.append((slug, RETRY, None, '%s. DETAILS IN %s' % (reason, LOG)))
|
|
NO_NOTE = ('complete', 'already')
|
|
for name, status, prog, detail in PHASES:
|
|
open('/tmp/fb', 'wb').close()
|
|
s = ffscreen.Screen('/tmp/fb', geometry=(W, H, BPP))
|
|
note = '' if (name in NO_NOTE or detail) else 'DO NOT TURN THE PRINTER OFF'
|
|
s.show('SETTING UP YOUR PRINTER', status, note, prog, detail, bool(detail))
|
|
buf = open('/tmp/fb', 'rb').read()
|
|
|
|
# Emit what the EYE sees, not the buffer: the panel is this portrait
|
|
# buffer turned 90 degrees clockwise, so undo that on the way out.
|
|
def px(bx, by):
|
|
off = by * s.stride + bx * 4
|
|
b, g, r, _ = buf[off:off + 4]
|
|
return bytes((r, g, b))
|
|
|
|
rows = []
|
|
for y in range(s.height):
|
|
row = bytearray(b'\x00')
|
|
for x in range(s.width):
|
|
row += px(y, s.buf_h - 1 - x)
|
|
rows.append(bytes(row))
|
|
|
|
def chunk(tag, data):
|
|
body = tag + data
|
|
return struct.pack('>I', len(data)) + body + struct.pack('>I', zlib.crc32(body))
|
|
|
|
png = (b'\x89PNG\r\n\x1a\n'
|
|
+ chunk(b'IHDR', struct.pack('>IIBBBBB', s.width, s.height,
|
|
8, 2, 0, 0, 0))
|
|
+ chunk(b'IDAT', zlib.compress(b''.join(rows), 9))
|
|
+ chunk(b'IEND', b''))
|
|
print('PNGSTART %s' % name)
|
|
print(base64.b64encode(png).decode())
|
|
print('PNGEND')
|
|
PYEOF
|