fpenrol.sh: no enrolment ever received the position guidance this script claims

pos() was eval'ing over the script's positional parameters from inside a
function, where $1 is the function's own argument. So `pos 3` printed "3" and
`pos 10` printed "100": every run this script has ever driven printed sample
numbers where positions should have been.

That rewrites an earlier conclusion rather than just fixing a bug. No enrolment
on this device had ever been guided, including the one that produced the best
template measured so far -- its spread came entirely from the config's overlap
band refusing near-duplicates, not from anything this script said. The
paired-test attribution drops a contributor, and the 30-sample run becomes
directly comparable to it because both were equally unguided.

Prompts are therefore opt-in now, behind FPENROL_GUIDE=1. The first run where
they actually fired steered the finger to edges, tips and hard rolls, needed 74
presses for 30 accepts, and produced a template that matched 0 of 10 taps.
Prompting a user toward positions they never use in a real tap builds a template
of positions they never use in a real tap. The default is the instruction that
worked: press as you would to unlock, and let the refusals move you.

The stage count comes from the daemon's num-enroll-stages property rather than a
second hardcoded 20.
This commit is contained in:
Jorijn van der Graaf 2026-09-03 17:44:35 +02:00
commit a44e0963ce

View file

@ -7,54 +7,106 @@
# roughly one position, and it matches roughly one frame in seven -- against
# 47-75% for the template the research harness enrolled. The algorithm was
# given ten near-duplicate images of one spot, so it recognises that spot and
# little else. It accepted them because our config set none of stock's
# enrolment-quality keys and the trustlet's own defaults accept anything;
# those keys are now set, so the algorithm enforces the spread too.
# little else.
#
# This walks a different contact position per accepted sample. It reads
# fprintd-enroll's per-stage output and prompts the next position as each
# stage passes, so the guidance tracks what the trustlet ACCEPTED rather than
# how many times you pressed.
#
# TWO BUGS FIXED 2026-09-03, both of which had silently disabled a feature
# this script claims to provide:
#
# 1. `pos()` was `eval "printf '%s' \"\$$1\""` over the script's positional
# parameters. Inside a FUNCTION, `$1` is the function's own argument, so
# `pos 3` printed "3" and `pos 10` printed `${1}0` = "100". Every run this
# script ever drove -- including the one that produced the 7/10 template --
# printed sample numbers where positions should have been, so NO enrolment
# has ever actually received position guidance. The spread in those
# templates came from the config's overlap band refusing near-duplicates,
# not from anything this script said. Positions now live in a newline
# list indexed with sed, which has no interaction with $1 at all.
#
# 2. The sample count was hardcoded to 20 while the daemon was configured for
# 30, so the counter froze at 20/20 and the run looked like it had hung or
# been clamped. It had not: all 30 samples were taken. The count now comes
# from the daemon's own num-enroll-stages property, which it reads from the
# same config the trustlet is given.
F=${1:-right-middle-finger}
# Position prompts are OPT-IN: FPENROL_GUIDE=1. The one template that verified
# well (right-middle, 20 samples, 7/10 quick taps) was enrolled with NO prompts
# -- the pos() bug meant none were ever shown -- and the algorithm's own
# overlap band did the spreading. The first run where prompts actually fired
# (right-middle, 30 samples, edges/tips/rolled-hard) needed 74 presses for 30
# accepts and then matched 0/10. Prompting a user toward positions they never
# use in a real tap builds a template of positions they never use in a real
# tap. Without prompts, the instruction is the one that worked: press the way
# you would to unlock, and let the REFUSED messages move you.
GUIDE=${FPENROL_GUIDE:-0}
# The daemon is the authority on how many stages there are.
STAGES=$(busctl get-property net.reactivated.Fprint \
/net/reactivated/Fprint/Device/0 \
net.reactivated.Fprint.Device num-enroll-stages 2>/dev/null \
| awk '{print $2}')
case "$STAGES" in
''|*[!0-9]*) STAGES=20
echo "warning: could not read num-enroll-stages; assuming $STAGES" ;;
esac
POSITIONS='centre of the pad
slightly LEFT of centre
slightly RIGHT of centre
higher up, toward the TIP
lower down, toward the JOINT
centre, rolled LEFT
centre, rolled RIGHT
tip, rolled slightly left
joint, rolled slightly right
centre, a little firmer
far LEFT edge of the pad
far RIGHT edge of the pad
very tip of the finger
well down toward the joint
upper left of the pad
upper right of the pad
lower left of the pad
lower right of the pad
centre, rolled hard left
centre, rolled hard right'
NPOS=$(printf '%s\n' "$POSITIONS" | wc -l)
# More samples than positions is expected at 30. Cycle, and say so, rather
# than printing nothing -- printing nothing is what hid bug 1 for two sessions.
pos() {
n=$(( ($1 - 1) % NPOS + 1 ))
printf '%s' "$(printf '%s\n' "$POSITIONS" | sed -n "${n}p")"
[ "$1" -gt "$NPOS" ] && printf ' (again, press slightly differently)'
}
i=1
# Twenty positions, stock's sample count. The config now enforces stock's
# coverage and overlap thresholds, so a sample that does not move enough from
# the last one is REFUSED by the algorithm rather than stored -- the guidance
# and the enforcement finally agree.
set -- "centre of the pad" \
"slightly LEFT of centre" \
"slightly RIGHT of centre" \
"higher up, toward the TIP" \
"lower down, toward the JOINT" \
"centre, rolled LEFT" \
"centre, rolled RIGHT" \
"tip, rolled slightly left" \
"joint, rolled slightly right" \
"centre, a little firmer" \
"far LEFT edge of the pad" \
"far RIGHT edge of the pad" \
"very tip of the finger" \
"well down toward the joint" \
"upper left of the pad" \
"upper right of the pad" \
"lower left of the pad" \
"lower right of the pad" \
"centre, rolled hard left" \
"centre, rolled hard right"
echo "enrolling $F with position guidance -- 20 samples (stock's count)"
echo "press FLAT and firm, hold about a second, then LIFT fully."
if [ "$GUIDE" = 1 ]; then
echo "enrolling $F with position guidance -- $STAGES samples (from the daemon)"
echo "press FLAT and firm, hold about a second, then LIFT fully."
prompt() { echo ">>> sample $1/$STAGES: $(pos $1)"; }
else
echo "enrolling $F -- $STAGES samples (from the daemon), NO position prompts"
echo "press the way you would to UNLOCK the phone. When a sample is REFUSED,"
echo "shift a little from where you were; otherwise do not think about it."
prompt() { echo ">>> sample $1/$STAGES"; }
fi
echo
# The reader runs in a subshell, so `shift` there cannot advance the caller's
# list. Index into it instead.
pos() { eval "printf '%s' \"\$$1\""; }
echo ">>> sample 1/20: $(pos 1)"
prompt 1
fprintd-enroll -f "$F" user 2>&1 | while IFS= read -r line; do
case "$line" in
*enroll-stage-passed*)
i=$((i+1))
if [ "$i" -le 20 ]; then
if [ "$i" -le "$STAGES" ]; then
echo " accepted."
prompt "$i"
else
echo " accepted."
echo ">>> sample $i/20: $(pos $i)"
fi ;;
*enroll-retry-scan*) echo " REFUSED -- move further from the last position, or press flatter" ;;
*enroll-completed*) echo; echo "COMPLETED." ;;