mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 12:52:16 -05:00
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
168 lines
6.7 KiB
Python
Executable File
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:]))
|