Created
May 17, 2025 20:34
-
-
Save tommie/1503f7eebd2b6e916ac5b038e38a4b78 to your computer and use it in GitHub Desktop.
Patch to recreate byte-perfect Prusa firmware images
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -eu | |
set -o pipefail | |
ORIGINAL_BBF=${ORIGINAL_BBF:-MINI_english-german_firmware_6.2.4.bbf} | |
# Could be inferred from the ORIGINAL_BBF | |
preset=mini-en-de | |
build_type=release | |
bootloader=yesboot | |
dev_items=no | |
if [ "x${1:-}" = "x--in-docker" ]; then | |
commit_nr=$(git rev-list HEAD --count) | |
version_suffix_short="+$commit_nr" | |
version_suffix_full="$version_suffix_short" | |
echo "Building ${preset} ${build_type} ${bootloader}" | |
ln -fs /work/.dependencies | |
ln -fs /work/.venv | |
. .venv/bin/activate | |
#rm -rf build | |
exec python3 utils/build.py \ | |
--preset ${preset} \ | |
--build-type ${build_type} \ | |
--bootloader ${bootloader%boot} \ | |
--generate-dfu \ | |
--no-store-output \ | |
--skip-bootstrap \ | |
--version-suffix=${version_suffix_full} \ | |
--version-suffix-short=${version_suffix_short} \ | |
-DCUSTOM_COMPILE_OPTIONS:STRING="-Werror" \ | |
-DDEVELOPMENT_ITEMS_ENABLED:BOOL=${dev_items} | |
fi | |
# See add_lfs_image(resources-image) in src/resources/CMakeLists.txt. | |
case "$preset" in | |
mini-*) | |
lfs_block_count=205 | |
;; | |
*) | |
lfs_block_count=512 | |
;; | |
esac | |
docker-run () { | |
docker run -it --rm --user $(id -u):$(id -g) -v "$PWD:/src" -w /src --name prusa-firmware-buddy-builder "${DOCKER_OPTS[@]}" prusa-firmware-buddy "$@" | |
} | |
get-image-ordering () { | |
local bbf=$1 resolved_bbf | |
resolved_bbf=$(readlink -f "$bbf") | |
( | |
tmpdir=$(mktemp -d -t prusa-buddy-unbuild.XXXXXXXXXX) | |
trap "rm -fr '$tmpdir'" EXIT | |
srcdir=$PWD | |
( | |
cd "$tmpdir" | |
# Extract the resource file system from the released firmware. | |
"$srcdir/utils/unpack_bbf.py" --input-file "$resolved_bbf" >&2 | |
) | |
DOCKER_OPTS=( -v "$tmpdir":/unbuild ) | |
mkdir "$tmpdir/resources-image" "$tmpdir/qoi-data" | |
# Extract the resources file system. | |
docker-run .venv/bin/littlefs-python unpack \ | |
--block-size 4096 \ | |
--block-count "$lfs_block_count" \ | |
--image /unbuild/resources-image.lfs \ | |
/unbuild/resources-image/ >&2 | |
# The bootloader file system only contains the bootloader image. Not interesting. | |
# docker-run .venv/bin/littlefs-python unpack --block-size 4096 --block-count 64 --image /unbuild/resources-bootloader-image.lfs /unbuild/resources-bootloader-image/ >&2 | |
# Get the ordering of the QOI image files. | |
docker-run .venv/bin/python3 qoi_unpacker.py \ | |
--resdir src/gui/res/png \ | |
--filterfile src/gui/res/mini_used_imgs.txt \ | |
/unbuild/resources-image/qoi.data \ | |
/unbuild/qoi-data >&2 | |
cat "$tmpdir/qoi-data/index.txt" | |
) | |
} | |
# Build the Docker builder container. This is used by Prusa's Jenkins, and also works for releases. | |
docker build --tag prusa-firmware-buddy -f utils/holly/Dockerfile . | |
mkdir -p build | |
get-image-ordering "$ORIGINAL_BBF" >build/qoi-ordering.txt | |
# Run the build. | |
docker-run bash "./$(basename $0)" --in-docker "$@" | |
# The BBF starts with the ECDSA signature, which we don't have the private key for. | |
git diff \ | |
--word-diff --no-index \ | |
<(hexdump --skip 64 -C "$ORIGINAL_BBF") \ | |
<(hexdump --skip 64 -C "build/${preset}_${build_type}_boot/firmware.bbf") | |
find build/products -ls | |
echo "Done. No diff (aside from the signature)!" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/CMakeLists.txt b/CMakeLists.txt | |
index 092a511d6..8026e4358 100644 | |
--- a/CMakeLists.txt | |
+++ b/CMakeLists.txt | |
@@ -357,6 +357,9 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") | |
add_compile_definitions(_DEBUG) | |
endif() | |
+add_compile_options(-Wno-builtin-macro-redefined) | |
+add_compile_definitions(__DATE__="Apr 14 2025") | |
+ | |
# disable unaligned access | |
# | |
# * Otherwise, with optimizations turned on, the firmware crashes on startup. | |
diff --git a/cmake/ProjectVersion.cmake b/cmake/ProjectVersion.cmake | |
index e94fd2330..6030fc047 100644 | |
--- a/cmake/ProjectVersion.cmake | |
+++ b/cmake/ProjectVersion.cmake | |
@@ -67,7 +67,7 @@ function(resolve_version_variables) | |
git_local_changes(IS_DIRTY) | |
if(${IS_DIRTY} STREQUAL "DIRTY") | |
set(FW_COMMIT_DIRTY | |
- TRUE | |
+ FALSE | |
PARENT_SCOPE | |
) | |
else() | |
diff --git a/src/resources/QoiGenerator.cmake b/src/resources/QoiGenerator.cmake | |
index 2e719a6f6..7e3a68467 100644 | |
--- a/src/resources/QoiGenerator.cmake | |
+++ b/src/resources/QoiGenerator.cmake | |
@@ -2,6 +2,7 @@ set(qoi_source_dir "${CMAKE_SOURCE_DIR}/src/gui/res/png") | |
set(qoi_generator_py "${CMAKE_SOURCE_DIR}/utils/qoi_packer.py") | |
set(qoi_data_file "${CMAKE_CURRENT_BINARY_DIR}/qoi.data") | |
set(qoi_resources_file "${CMAKE_BINARY_DIR}/src/gui/res/qoi_resources.gen") | |
+set(qoi_order_from_arg "-order_from=${CMAKE_BINARY_DIR}/../qoi-ordering.txt") | |
if(PRINTER STREQUAL "MINI") | |
# mini only loads images that are actually used, because it has small xflash | |
@@ -12,7 +13,7 @@ endif() | |
add_custom_command( | |
OUTPUT "${qoi_data_file}" "${qoi_resources_file}" | |
COMMAND "${Python3_EXECUTABLE}" "${qoi_generator_py}" "${qoi_source_dir}" "${qoi_resources_file}" | |
- "${qoi_data_file}" ${qou_used_files_arg} | |
+ "${qoi_data_file}" ${qou_used_files_arg} ${qoi_order_from_arg} | |
DEPENDS "${qoi_source_dir}" "${qoi_generator_py}" "${qoi_used_files}" | |
VERBATIM | |
) | |
diff --git a/utils/qoi_packer.py b/utils/qoi_packer.py | |
index 95e188739..e9df521fe 100755 | |
--- a/utils/qoi_packer.py | |
+++ b/utils/qoi_packer.py | |
@@ -25,17 +25,26 @@ def main(): | |
help='Path to file with list of actually used images in this printer', | |
default=None, | |
required=False) | |
+ parser.add_argument( | |
+ '-order_from', | |
+ type=Path, | |
+ help='Path to file with list of preferred image order') | |
args = parser.parse_args() | |
png_files = [] | |
if os.path.isdir(args.input.resolve()): | |
- for filename in os.listdir(args.input.resolve()): | |
+ for filename in sorted(os.listdir(args.input.resolve())): | |
if filename.endswith('.png'): | |
png_files.append((args.input.resolve() / filename).resolve()) | |
else: | |
png_files.append(args.input.resolve()) | |
+ if args.order_from: | |
+ with args.order_from.open() as f: | |
+ ordering = {name.strip(): i for i, name in enumerate(f)} | |
+ png_files.sort(key=lambda path: ordering.get(path.name, len(ordering))) | |
+ | |
filtered_pngs = None | |
if (args.input_filter): | |
filtered_pngs = [] | |
diff --git a/utils/unpack_bbf.py b/utils/unpack_bbf.py | |
index 8b4d22896..343600ca8 100755 | |
--- a/utils/unpack_bbf.py | |
+++ b/utils/unpack_bbf.py | |
@@ -72,9 +72,12 @@ def main(): | |
print(f'printer type: {printer_type}') | |
print(f'printer version: {printer_version}') | |
print(f'printer subversion: {printer_subversion}') | |
+ print(f'tlvs: {list(tlv.keys())}') | |
write_bytes_to_file('firmware.bin', firmware_code) | |
write_bytes_to_file('resources-image.lfs', tlv.get(1, bytes())) | |
+ if 5 in tlv: | |
+ write_bytes_to_file('resources-bootloader-image.lfs', tlv[5]) | |
return 0 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
''' | |
Unpacks a qoi.data file packed by qoi_packer.py. | |
Optionally uses the src/gui/res/png/ directory to figure out the original names | |
of the files. This is useful to figure out the file ordering. | |
''' | |
import argparse | |
import hashlib | |
import io | |
import pathlib | |
import numpy | |
from PIL import Image | |
import qoi | |
def png_to_qoi(data: bytes): | |
img = Image.open(io.BytesIO(data), formats=['png']).convert('RGBA') | |
return qoi.encode(numpy.array(img)) | |
def main(): | |
argp = argparse.ArgumentParser() | |
argp.add_argument('--png', action='store_true') | |
argp.add_argument('--resdir', type=pathlib.Path, help='Sets the src/gui/res/ directory to use for figuring out the original names', default='src/gui/res/png') | |
argp.add_argument('--filterfile', type=pathlib.Path) | |
argp.add_argument('infile', type=pathlib.Path) | |
argp.add_argument('outdir', type=pathlib.Path) | |
args = argp.parse_args() | |
if args.resdir: | |
res = {f.name: png_to_qoi(f.read_bytes()) | |
for f in args.resdir.glob('*.png')} | |
else: | |
res = {} | |
if args.filterfile: | |
newres = {} | |
with args.filterfile.open() as f: | |
for name in f: | |
name = name.strip() | |
if name in res: | |
newres[name] = res[name] | |
res = newres | |
res_by_hash = {hashlib.sha256(v).digest(): k for k, v in res.items()} | |
qoi_data = args.infile.read_bytes() | |
offset = 0 | |
index = 0 | |
with open(args.outdir / 'index.txt', 'w') as indexf: | |
while offset < len(qoi_data): | |
if qoi_data[offset:offset+4] != b'qoif': | |
raise ValueError(f'Expected to find a QOI file at {offset}: {qoi_data[offset:offset+4]}') | |
next_offset = qoi_data.find(b'qoif', offset + 14) | |
if next_offset == -1: next_offset = len(qoi_data) | |
size = next_offset - offset | |
orig = res_by_hash.get(hashlib.sha256(qoi_data[offset:next_offset]).digest(), None) | |
print(f'QOI file {index} at {offset} ({size} bytes) {orig}') | |
if orig: | |
print(orig, file=indexf) | |
else: | |
print(f'{index:03d}.png', file=indexf) | |
decoded = qoi.decode(qoi_data[offset:next_offset]) | |
if args.png: | |
image = Image.fromarray(decoded) | |
image.save(args.outdir / f'{index:03d}.png', 'png') | |
else: | |
with open(args.outdir / f'{index:03d}.qoi', 'wb') as outf: | |
outf.write(qoi_data[offset:next_offset]) | |
offset = next_offset | |
index += 1 | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment