mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-19 03:49:50 -05:00
Add Fluidd and custom boot scripts, migrate DC24V control, update the web UIs and filament macros, improve startup status reporting, and expand upgrade and replica coverage.
172 lines
6.9 KiB
Python
Executable File
172 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Render the 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 the normal boot and
|
|
# first-boot calibration phases 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 = [
|
|
('launching-services', 'LAUNCHING SERVICES', 0.02),
|
|
('mcu-boards', 'WAKING THE TOOLHEAD BOARDS', 0.12),
|
|
('mcu-heater', 'WAKING THE HEATER BOARD', 0.18),
|
|
('mcu-both', 'WAKING THE HEATER BOARD AND THE LEVEL BOARD', 0.18),
|
|
('starting-services', 'STARTING SERVICES', 0.30),
|
|
('moonraker', 'STARTING MOONRAKER', 0.45),
|
|
('klipper', 'STARTING KLIPPER', 0.60),
|
|
('ready', 'KLIPPER IS READY', 0.78),
|
|
('importing', 'READING FACTORY CALIBRATION', 0.82),
|
|
('saving', 'SAVING CALIBRATION', 0.88),
|
|
('restarting', 'RESTARTING KLIPPER', 0.94),
|
|
('complete', 'STARTUP COMPLETE', 1.0),
|
|
]
|
|
NO_NOTE = ('complete',)
|
|
|
|
# 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 = 'STARTUP WILL RETRY ON NEXT BOOT'
|
|
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)
|
|
calibration = name in ('importing', 'saving', 'restarting')
|
|
note = ('' if (name in NO_NOTE or detail) else
|
|
'DO NOT TURN THE PRINTER OFF' if calibration else 'PLEASE WAIT')
|
|
screen.show('REFORGE IS STARTING', 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:]))
|