Files
firmware/bin/preview-boot-screen.py
Oleksandr ShyshatskyiandClaude Opus 5 9b5b562a22 Comments: keep the contract and the trap, drop the narration
A sweep over every commented file in the tree. What is kept is what a
reader needs to change the code safely: what a thing is, its signature,
the invariants other files depend on, and the traps that fail silently
(find's non-zero exit under set -e, s6's baked-in libexecdir, the -lm
that decides whether _sqlite3 exists, opkg's compile-time prefix).

What goes is the narration around it: incident histories, arguments
against alternatives already rejected, restatements of the code below,
and paragraphs re-explaining a rule three files already state. One
paragraph in pkgs/3rdparty/opkg/build.sh was duplicated outright.

Comment lines 7971 -> 6158. Every change is comment-only: no
non-comment line differs anywhere in the 86 files, checked
mechanically. Commented-out config directives (#fast_feed: 30000 and
friends) are documentation of defaults and are kept.

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

168 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
# Render the first-boot screen to PNGs so you can LOOK at it.
#
# payload/bin/ffscreen.py draws onto a framebuffer, which is a flat array of
# pixels and therefore untestable by reading the code. This wraps it in the
# smallest possible harness -- a file standing in for /dev/fb0 -- and converts
# the result to a PNG with nothing but the standard library.
#
# ./bin/preview-boot-screen.py [--out DIR] [--size 480x800@32]
#
# The default size is the real one: the framebuffer is PORTRAIT 480x800 and
# the panel is that buffer turned 90 degrees clockwise, so what is written
# here is rotated back before it becomes a PNG. It renders every phase the
# migration goes through, in order.
import argparse
import glob
import importlib.util
import os
import struct
import sys
import zlib
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Every (status, progress) ff-startup.py can put on the panel, in the
# order it happens. Keep this in step with the panel.say() calls there.
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),
]
NO_NOTE = ('complete', 'already')
# Every way the migration can end badly, with the reason it puts on the panel.
# Kept in step with the panel.failed() calls in ff-startup.py.
FAILURES = [
('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'),
]
RETRY = 'SETUP WILL RETRY ON NEXT START'
LOGFILE = '/USR/DATA/LOGS/ANVIL-BOOT.LOG'
def load_ffscreen():
path = os.path.join(ROOT, 'pkgs', 'anvil-core', 'payload', 'bin',
'ffscreen.py')
spec = importlib.util.spec_from_file_location('ffscreen', path)
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):
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.
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(scanlines, 6)))
fh.write(chunk(b'IEND', b''))
def to_rows(buf, screen):
"""Framebuffer bytes -> one RGB triple per pixel, per row, AS DISPLAYED.
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.
"""
bytes_per_pixel = screen.bpp // 8
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 = []
for y in range(screen.height):
row = bytearray()
for x in range(screen.width):
if screen.rotate == 90:
row += pixel(y, screen.buf_h - 1 - x)
elif screen.rotate == 270:
row += pixel(screen.buf_w - 1 - y, x)
else:
row += pixel(x, y)
rows.append(bytes(row))
return rows
def main(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)
if geometry is None:
raise SystemExit('--size wants WxH@BPP, e.g. 1024x600@32')
# Clear first: a renamed or removed phase would otherwise leave its old
# frame lying in the directory, and a stale frame in a gallery of current
# ones is worse than a missing one.
os.makedirs(args.out, exist_ok=True)
for stale in glob.glob(os.path.join(args.out, '*.png')):
os.remove(stale)
frames = [(n, s, p, '') for n, s, p in PHASES]
frames += [(n, RETRY, None, '%s. DETAILS IN %s' % (r, LOGFILE))
for n, r in FAILURES]
fb = os.path.join(args.out, 'fb0.raw')
for name, status, progress, detail in frames:
open(fb, 'wb').close()
screen = ffscreen.Screen(fb, geometry=geometry, rotate=args.rotate)
if not screen.ok:
raise SystemExit('ffscreen refused this geometry: %s' % args.size)
note = '' if (name in NO_NOTE or detail) else 'DO NOT TURN THE PRINTER OFF'
screen.show('SETTING UP YOUR PRINTER', status, note, progress,
detail, bool(detail))
with open(fb, 'rb') as fh:
buf = fh.read()
out = os.path.join(args.out, '%s.png' % name)
png(out, screen.width, screen.height, to_rows(buf, screen))
print('%-30s %s (%dx%d panel, %dx%d buffer)'
% (status, out, screen.width, screen.height,
screen.buf_w, screen.buf_h))
os.remove(fb)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))