Keep a pristine copy of the flash image, and a way back to it

assets/flash.img was gitignored, so the only copy of the never-booted image lived
on one disk. The emulator writes to that image, so a session can leave edited
settings or a damaged EEPROM behind with nothing to restore from.

assets/pristine/ now holds the image as first generated, gzipped and checksummed,
and is tracked deliberately. Gzip takes it from 2 MiB to 2.3 KiB because the image
is nearly all 0xFF, which is what makes keeping it in git reasonable. The live
image and its .bak-* files stay ignored.

Two checksums are recorded, for the archive and for its contents, so a corrupted
archive is distinguishable from one that was replaced.

tools/restore_flash.sh verifies, diffs, or restores. Restore backs up the current
image first, then re-checks the result, since a restore that silently half-worked
would be worse than none.

Verified by deliberately corrupting the live image: --diff reported 32 differing
bytes, restore backed up and rewrote it, and --diff then reported no change. The
image is currently byte-identical to what make_flash.py produces, so this is the
genuine original rather than a copy of something already used.
This commit is contained in:
mckero committed 2026-08-28 12:20:13 +01:00
1 parent 19997e85e1
commit 1364d46e97
5 files changed
+102 -1

No files matched your search

+5
View File
@@ -1,5 +1,10 @@
# Generated flash image: 2 MB, rebuilt from calibration.bin by tools/make_flash.py. # Generated flash image: 2 MB, rebuilt from calibration.bin by tools/make_flash.py.
# The live image is a build artifact and the emulator writes to it.
assets/flash.img assets/flash.img
assets/flash.img.bak-*
# ...but assets/pristine/ is tracked on purpose: it is the never-booted
# reference to fall back to when a session leaves the image in a bad state.
!assets/pristine/
# Host build output for the CW timing harness. # Host build output for the CW timing harness.
build/ build/
+15 -1
View File
@@ -65,7 +65,7 @@ keypresses silently stop working. Run the test after touching that code;
qemu/ QEMU sources to be copied into a QEMU tree qemu/ QEMU sources to be copied into a QEMU tree
py32f071.c the SoC and machine (the bulk of the work) py32f071.c the SoC and machine (the bulk of the work)
armv7m_systick.*.patched SysTick with the poll-boost property added armv7m_systick.*.patched SysTick with the poll-boost property added
assets/ assets/ flash.img, plus pristine/ as the reference copy
calibration.bin 512-byte dump from a real radio calibration.bin 512-byte dump from a real radio
deploy/ nginx vhost for the HTTPS front end deploy/ nginx vhost for the HTTPS front end
docs/reverse-proxy.md how https://k6v3.mckero.dn42/ is served docs/reverse-proxy.md how https://k6v3.mckero.dn42/ is served
@@ -74,6 +74,7 @@ keypresses silently stop working. Run the test after touching that code;
keypad_test.py keypad regression test, boots its own instance keypad_test.py keypad regression test, boots its own instance
webui.py web remote control: live LCD plus clickable keypad webui.py web remote control: live LCD plus clickable keypad
dn42_firewall.sh restrict the web UI port to DN42 sources dn42_firewall.sh restrict the web UI port to DN42 sources
restore_flash.sh roll the flash image back to its pristine state
uvk5_qmp.py QMP client uvk5_qmp.py QMP client
uvk5_lcd.py framebuffer decode, PNG encode, frame grabber uvk5_lcd.py framebuffer decode, PNG encode, frame grabber
uvk5_keys.py key names the keypad model accepts uvk5_keys.py key names the keypad model accepts
@@ -124,6 +125,19 @@ The rest of the tests:
## Running ## Running
python3 tools/make_flash.py # once, builds assets/flash.img python3 tools/make_flash.py # once, builds assets/flash.img
The emulator writes to that image, so a session can leave edited settings or a
damaged EEPROM behind. `assets/pristine/` holds a checksummed copy of the image as
first generated, and `tools/restore_flash.sh` puts it back:
tools/restore_flash.sh --verify # is the reference copy itself intact
tools/restore_flash.sh --diff # has the live image changed, and by how much
tools/restore_flash.sh # restore, saving the current image first
The reference copy is stored gzipped, which takes 2.3 KiB rather than 2 MiB because
the image is nearly all 0xFF, so it is small enough to keep in git. The live image
stays ignored: it is a build artifact that gets written to.
tools/run.sh # starts the machine tools/run.sh # starts the machine
tools/where.sh # where the firmware is executing tools/where.sh # where the firmware is executing
Binary file not shown.
@@ -0,0 +1,2 @@
933d697426baee836ff15a6eea6ab7c23f98be3d73e3c5ec9957f6818f393d62 flash-pristine.img
d8e7551c75c1f85681d9f98a36b0f6d7f287b8d4c599b1475b271a292cd2d0d8 flash-pristine.img.gz
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Restore assets/flash.img to its pristine, never-booted state.
#
# The emulator writes to the flash image, so a session can leave settings, edited
# frequencies, or a corrupted EEPROM behind. assets/pristine/ holds a checksummed
# copy of the image as first generated, so there is always a known-good state to
# come back to.
#
# Usage:
# tools/restore_flash.sh # restore, backing up the current image
# tools/restore_flash.sh --verify # only check the pristine copy is intact
# tools/restore_flash.sh --diff # show whether the live image has changed
set -euo pipefail
SIM="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LIVE="$SIM/assets/flash.img"
PRISTINE_GZ="$SIM/assets/pristine/flash-pristine.img.gz"
SUMS="$SIM/assets/pristine/flash-pristine.img.sha256"
want_plain() { awk '$2 == "flash-pristine.img" {print $1}' "$SUMS"; }
want_gz() { awk '$2 == "flash-pristine.img.gz" {print $1}' "$SUMS"; }
verify_pristine() {
[ -f "$PRISTINE_GZ" ] || { echo "missing $PRISTINE_GZ" >&2; exit 1; }
local got_gz got_plain
got_gz=$(sha256sum "$PRISTINE_GZ" | awk '{print $1}')
if [ "$got_gz" != "$(want_gz)" ]; then
echo "pristine archive is CORRUPT" >&2
echo " expected $(want_gz)" >&2
echo " actual $got_gz" >&2
exit 1
fi
got_plain=$(gzip -dc "$PRISTINE_GZ" | sha256sum | awk '{print $1}')
if [ "$got_plain" != "$(want_plain)" ]; then
echo "pristine contents do not match their checksum" >&2
exit 1
fi
echo "pristine copy verified ($(want_plain))"
}
case "${1:-restore}" in
--verify)
verify_pristine
;;
--diff)
verify_pristine
if [ ! -f "$LIVE" ]; then
echo "no live image at $LIVE"
exit 0
fi
live=$(sha256sum "$LIVE" | awk '{print $1}')
if [ "$live" = "$(want_plain)" ]; then
echo "live image is unchanged from pristine"
else
echo "live image HAS CHANGED from pristine"
echo " pristine $(want_plain)"
echo " live $live"
gzip -dc "$PRISTINE_GZ" > /tmp/.pristine.$$
echo " differing bytes: $(cmp -l /tmp/.pristine.$$ "$LIVE" 2>/dev/null | wc -l)"
rm -f /tmp/.pristine.$$
fi
;;
restore)
verify_pristine
if [ -f "$LIVE" ]; then
backup="$LIVE.bak-$(date +%Y%m%d-%H%M%S)"
cp -a "$LIVE" "$backup"
echo "current image saved to $(basename "$backup")"
fi
gzip -dc "$PRISTINE_GZ" > "$LIVE"
got=$(sha256sum "$LIVE" | awk '{print $1}')
[ "$got" = "$(want_plain)" ] || { echo "restore verification FAILED" >&2; exit 1; }
echo "restored $LIVE to pristine state"
echo "note: the emulator reads the image at boot, so power-cycle to pick it up"
;;
*)
echo "usage: $0 [restore|--verify|--diff]" >&2
exit 2
;;
esac