Skip to content

Instantly share code, notes, and snippets.

@tommie
Created May 17, 2025 20:34
Show Gist options
  • Save tommie/1503f7eebd2b6e916ac5b038e38a4b78 to your computer and use it in GitHub Desktop.
Save tommie/1503f7eebd2b6e916ac5b038e38a4b78 to your computer and use it in GitHub Desktop.
Patch to recreate byte-perfect Prusa firmware images
#!/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)!"
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
#!/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