fingerprintd/packaging/fplearn.sh
Jorijn van der Graaf f3da01a511 Snapshot and restore a template, so an improvement can actually be attributed
Learning rewrites the template in place, which means a rate that climbs over an
evening has three explanations and the numbers cannot tell them apart: the
template got better, the sensor got wiped clean, or the user learned where the
sensor likes to be pressed. Jorijn named the second and third while the numbers
were still going up. All three move the same way on the same time axis, and the
first trend run had no way to separate them because no earlier version of the
template survived.

A snapshot makes the paired test possible. Measure the learned template, restore
the older one, measure again within the same session -- the sensor is equally
clean and the user equally practised across both halves, so the only thing that
differs is the template. A rate that falls back on restore is learning. A rate
that stays up is not.

Restore stops the daemon before swapping the files and brings it back with
learning off, because the trustlet caches the template in memory once loaded and
because a restored template that immediately starts learning again is not a
control.
2026-09-04 23:49:25 +02:00

213 lines
9.3 KiB
Shell
Executable file

#!/bin/sh
# fplearn.sh -- the template-learning measurement, as a protocol rather than a
# pile of remembered commands.
#
# fplearn.sh snapshot [name] save the current template aside
# fplearn.sh restore <name> put a saved template back and reload it
# fplearn.sh wipe remove every stored template (backup first)
# fplearn.sh enrol [finger] enrol at the config's sample count
# fplearn.sh base [n] [w] trial with learning OFF (the baseline)
# fplearn.sh trend [n] [w] three trials with learning ON
# fplearn.sh sizes just print the template container sizes
#
# SNAPSHOT BEFORE EVERY MEASUREMENT BLOCK. Learning rewrites the template in
# place, so without a snapshot an improvement cannot be attributed: a rate that
# climbs over an evening is equally explained by the template getting better, by
# the SENSOR being wiped clean, or by the USER learning where the sensor likes
# to be pressed (Jorijn, 2026-09-04, having spotted both while the numbers were
# still going up). All three move the same way on the same time axis.
#
# A snapshot makes the paired test possible: measure the learned template, then
# restore the older one and measure again WITHIN THE SAME SESSION, so the sensor
# is equally clean and the user equally practised for both halves. A rate that
# falls back on restore is learning. A rate that stays up is not.
#
# WIPE FIRST when re-enrolling a finger that is already in the group. Measured
# 2026-09-04: the duplicated-finger check refuses a finger the group already
# holds -- 0 accepted of 7 presses, rc=0 on every one, while a never-enrolled
# finger progressed normally. Re-enrolment ADDS a template and there is no
# trustlet-side remove, so the old one has to go first. `enrol` refuses a
# finger name that is already in the map and points here.
#
# WHY IT IS SHAPED LIKE THIS. Learning is cumulative: every matched press folds
# frames into the stored template, so a second run is not a repeat of the first
# and an A/B against a moving template is not an A/B at all. The only honest
# comparison is on ONE template lineage, in order:
#
# 1. enrol a fresh template, 20 samples, no position prompts --
# which is also the outstanding replication of the 7/10
# result, the one measurement this lane was parked on
# 2. base learning off: the clean number for THIS template, and
# the only figure comparable to every rate in the journal
# 3. trend learning on, three times: the rate should climb, and
# the container size is an independent witness that it
# is the template moving and not the weather
#
# Do NOT read run 1 of the trend as "learning made it better". Run 1 starts on
# the same template the baseline ended on; it is the first run that can improve
# it, not one that has already been improved.
set -u
GROUP=/mnt/persist/data/RIY7A+mQm3EA4FsCUmkJo0b9dFUYP2YZ4P5hmMiZgeA_Alt
MAP=/var/lib/fingerprintd/fingers-10000.map
UNIT=fingerprintd-test
BIN=/tmp/fingerprintd
COMMON="--daemon --verbose --edge-wake --sfs-root=/var/lib/fingerprintd/sfs --sfs-writable --rpmb-write"
# How many frames learning actually folded, out of the daemon's own transcript.
# Without this a trend run cannot tell "learning fired and did not help" from
# "learning never fired" -- which is exactly the confusion that made the first
# trend run measure a static template three times.
folds() {
L=$(ls -t /var/log/fingerprintd/*.log 2>/dev/null | head -1)
[ -n "$L" ] || { echo " (no transcript)"; return; }
m=$(grep -c -- "-> MATCH fid=" "$L" 2>/dev/null || echo 0)
f=$(grep -c "folded in (metric" "$L" 2>/dev/null || echo 0)
s=$(grep -c "learn: template saved" "$L" 2>/dev/null || echo 0)
echo " folds: $f frame(s) over $m matched press(es), $s save(s)"
}
sizes() {
# A template is stored twice, the container and its backup, so the sizes
# come in pairs. The BODY is the container minus its 4096-byte header.
sudo ls -la "$GROUP" 2>/dev/null | awk '$5 > 200000 { printf " %9d body %9d %s\n", $5, $5-4096, $9 }' | sort -u
}
restart() { # $1 = extra args
sudo systemctl stop "$UNIT" 2>/dev/null
sleep 2
sudo systemd-run --unit="$UNIT" --collect $BIN $COMMON $1 >/dev/null 2>&1
printf 'daemon starting'
i=0
while [ $i -lt 40 ]; do
if busctl --system list 2>/dev/null | grep -q net.reactivated.Fprint; then
# Owning the name is not the same as being ready: the session comes
# up on the worker thread afterwards.
sleep 6; printf ' ready\n'; return 0
fi
printf '.'; sleep 1; i=$((i+1))
done
printf ' TIMED OUT\n'; return 1
}
SNAPDIR=$HOME/fp6-backups/snapshots
case "${1:-}" in
snapshot)
NAME=${2:-$(date +%Y%m%d-%H%M%S)}
D=$SNAPDIR/$NAME
mkdir -p "$D"
n=0
for f in $(sudo ls "$GROUP"); do
sz=$(sudo stat -c %s "$GROUP/$f")
if [ "$sz" -gt 200000 ]; then sudo cp -p "$GROUP/$f" "$D/$f"; n=$((n+1)); fi
done
sudo cp -p "$MAP" "$D/" 2>/dev/null || true
sudo chown -R "$(id -u):$(id -g)" "$D"
echo "snapshot '$NAME': $n container(s)"
ls -la "$D" | awk 'NR>3{printf " %9d %s\n", $5, $9}'
echo "restore with: fplearn.sh restore $NAME" ;;
restore)
NAME=${2:-}
D=$SNAPDIR/$NAME
[ -n "$NAME" ] && [ -d "$D" ] || { echo "usage: fplearn.sh restore <name>"; echo "available:"; ls "$SNAPDIR" 2>/dev/null | sed 's/^/ /'; exit 1; }
echo "=== restore snapshot '$NAME' ==="
echo "before:"; sizes
# The trustlet holds the template in memory once loaded, so a restore that
# does not restart the daemon changes the file and nothing else.
sudo systemctl stop "$UNIT" 2>/dev/null; sleep 2
for f in $(ls "$D"); do
case "$f" in fingers-*.map) sudo cp -p "$D/$f" "$MAP" ;;
*) sudo cp -p "$D/$f" "$GROUP/$f" ;; esac
done
sudo sync
restart "--learn=0" || exit 1
echo "after:"; sizes
fprintd-list user 2>&1 | tail -1
echo
echo "Daemon is up with learning OFF, so the restored template stays put."
echo "Measure it now, in this session, against the block you just ran:"
echo " fplearn.sh base" ;;
sizes)
echo "template containers now:"; sizes ;;
wipe)
# The procedure of 2026-09-03 and 2026-09-04, and its reasoning: ONLY the
# template containers go (>200000 bytes; each template is stored twice).
# The 1588-byte group index is KEPT -- deleting it risks the RPMB
# anti-rollback counters going stale against object ids QTEE would then
# recreate, which already cost an index restore once -- and a missing
# template container is the known-good "not exist, skip" case. The daemon
# is stopped first so the trustlet reloads from the store, and a backup is
# taken first because a template is not reproducible without a finger.
echo "=== wipe: every stored template ==="
echo "before:"; sizes
D=$HOME/fp6-backups/$(date +%Y-%m-%d-%H%M)-pre-wipe
mkdir -p "$D"
sudo tar -cf "$D/persist-data.tar" -C /mnt/persist data
sudo cp "$MAP" "$D/" 2>/dev/null || true
sudo chown -R "$(id -u):$(id -g)" "$D"
echo "backup: $D ($(tar -tf "$D/persist-data.tar" | wc -l) entries, $(sha256sum "$D/persist-data.tar" | cut -c1-16))"
sudo systemctl stop "$UNIT" 2>/dev/null; sleep 2
n=0
for f in $(sudo ls "$GROUP"); do
sz=$(sudo stat -c %s "$GROUP/$f")
if [ "$sz" -gt 200000 ]; then sudo rm -f "$GROUP/$f"; n=$((n+1)); fi
done
sudo sync
sudo sh -c ": > $MAP"
echo "removed $n container(s); index and small containers kept; map cleared"
restart "--learn=1" || exit 1
fprintd-list user 2>&1 | tail -1
echo "after:"; sizes
echo; echo "next: fplearn.sh enrol <finger>" ;;
enrol)
F=${2:-right-middle-finger}
if sudo grep -q "^$F " "$MAP" 2>/dev/null; then
echo "$F is already enrolled (in $MAP)."
echo "The trustlet REFUSES to re-enrol a finger it already holds; run"
echo " fplearn.sh wipe"
echo "first. (Measured 2026-09-04: 0 accepted of 7 presses, rc=0 each.)"
exit 1
fi
echo "=== enrol $F ==="
echo "sizes before:"; sizes
restart "--learn=1" || exit 1
/tmp/fpenrol.sh "$F"
echo; echo "sizes after the enrolment:"; sizes
echo; echo "next: fplearn.sh base" ;;
base)
N=${2:-15}; W=${3:-0}
echo "=== BASELINE: learning OFF ==="
restart "--learn=0" || exit 1
echo "sizes before:"; sizes
/tmp/fptrial.sh "$N" "$W"
echo; echo "sizes after (MUST be unchanged -- learning was off):"; sizes
echo; echo "next: fplearn.sh trend" ;;
trend)
# No wrong-finger taps by default: zero false accepts is settled over forty
# of them, and the presses are better spent on the false-negative rate.
N=${2:-15}; W=${3:-0}
echo "=== TREND: learning ON, three runs on one template ==="
restart "--learn=1" || exit 1
echo "sizes at the start:"; sizes
r=1
while [ $r -le 3 ]; do
echo; echo "----- learning run $r of 3 -----"
/tmp/fptrial.sh "$N" "$W"
echo "after run $r:"; sizes; folds
r=$((r+1))
done
echo
echo "Read it as a trend, not three numbers. A rate that climbs while the"
echo "container grows is learning; a rate that moves while the container"
echo "does not is noise, and the daemon's own 'learn:' lines say which." ;;
*)
sed -n '2,52p' "$0"; exit 1 ;;
esac