Guide the enrolment, and take the sample total from the config

Two problems from a real attempt, one mine and one the tool failing to explain
itself.

A sample is taken on the RISING edge only. Holding the finger down produces no
further touch events however long it stays there, so a run with the finger
almost permanently down collects one sample: 55 finger frames across 60, three
touch events, two samples accepted. The loop now says which state it is in on
every line -- press, hold, or LIFT -- shows accepted-of-total as it goes, and
calls out a finger that has been held for several frames, because that is the
state where nothing is happening and nothing on screen said so.

And the total is now read from the config instead of inferred. `rem` is
reported after the sample is processed, so the first reading of a healthy
enrolment is already 9, and a session that takes the first reading as its total
is permanently off by one -- it reported "1 of 9 accepted" when two samples had
been accepted out of ten. common.max_enrolling_samples is stated explicitly in
the generated config so both sides agree on the number rather than one of them
guessing.

Also recorded: not every press is accepted. The third touch of that run
reported the same count as the second, which is the algorithm rejecting a
sample, and is normal.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 21:01:00 +02:00
commit bdd5de21b3
3 changed files with 48 additions and 18 deletions

View file

@ -57,6 +57,7 @@ bool g_auth = false;
bool g_enrol = false;
int g_frames = 40;
int g_frameGapMs = 500;
int g_samples = 10; // common.max_enrolling_samples, as shipped
std::string g_logDir = "/var/log/fingerprintd";
int g_rescan = -1; // -1 = leave the config's value alone
@ -1245,7 +1246,8 @@ int Probe() {
// every held frame instead gives the algorithm near-duplicate images
// from a single press.
en::TouchTracker tracker;
en::EnrolSession enrol;
en::EnrolSession enrol(g_samples);
int heldFrames = 0;
for (int i = 0; i < g_frames && !enrol.Complete(); i++) {
std::vector<std::byte> q(0x10, std::byte{0});
SendCommand(app, ta::Cmd::QueryEventStatus, q);
@ -1255,6 +1257,13 @@ int Probe() {
auto c = SendCommand(app, ta::Cmd::CaptureImage, cap);
bool finger = baseline.IsFinger(c.metric);
// A sample is taken on the RISING edge only. Holding the finger
// down produces no further touch events however long it stays, so
// a run where the finger is never lifted collects exactly one
// sample -- which is what a first attempt at this did, 55 finger
// frames and three touches.
heldFrames = finger ? heldFrames + 1 : 0;
std::string note;
for (ta::Event ev : tracker.Observe(finger, en::Mode::Enrol)) {
std::vector<std::byte> evbuf(ta::EventContextSize);
@ -1273,8 +1282,15 @@ int Probe() {
}
SendCommand(app, ta::Cmd::QueryEventStatus, q);
std::println(" frame {:2}: metric={:<4}{}{}", i + 1, c.metric,
finger ? " FINGER" : " ", note);
std::println(" [{:2}/{}] {:<28} metric={:<4}{}{}",
enrol.Accepted(), enrol.Total(),
enrol.Started()
? (finger ? "hold... then LIFT" : "LIFT -- now press again")
: "press your finger",
c.metric, finger ? " FINGER" : " ", note);
if (heldFrames == 4)
std::println(" *** still held -- LIFT the finger, a sample is only "
"taken when you press again ***");
std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs));
}
@ -1332,6 +1348,7 @@ int main(int argc, char** argv) {
if (a.starts_with("--log-dir=")) g_logDir = a.substr(10);
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
if (a.starts_with("--group-path=")) g_groupPath = a.substr(13);
if (a.starts_with("--samples=")) g_samples = std::stoi(std::string(a.substr(10)));
if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11);
if (a.starts_with("--gid=")) g_gid = static_cast<std::uint32_t>(
std::stoul(std::string(a.substr(6))));

View file

@ -132,14 +132,22 @@ export namespace fingerprintd::engine {
// the value.
class EnrolSession {
public:
// The total is common.max_enrolling_samples and is KNOWN from the
// config, not inferred. Inferring it from the first reading is off by
// one: `rem` is reported after the sample has been processed, so the
// first observation of a healthy enrolment is already 9, not 10, and a
// session that takes 9 as the total reports one fewer accepted sample
// than actually happened.
explicit EnrolSession(std::int32_t total) : total_(total) {}
void Observe(std::int32_t remaining, bool fromEnrolPath) {
if (!fromEnrolPath) return; // a release reports nothing
if (remaining < 0) return; // not populated at all
if (remaining > total_) return; // nonsense
if (!started_) {
// A first reading of 0 is an unpopulated field, not a finished
// enrolment: the count starts at max_enrolling_samples.
// enrolment: the count starts at the total.
if (remaining == 0) return;
total_ = remaining;
remaining_ = remaining;
started_ = true;
return;
@ -161,6 +169,8 @@ export namespace fingerprintd::engine {
bool started_ = false;
std::int32_t total_ = 0;
std::int32_t remaining_ = -1;
// Set once the total is known; see the constructor.
};
// ---- Authentication accounting ----------------------------------------

View file

@ -138,16 +138,18 @@ int main() {
// ---- Enrolment progress, read from the response
{
EnrolSession e;
EnrolSession e(10);
Check(!e.Started(), "not started");
e.Observe(-1, true);
Check(!e.Started(), "an unpopulated field does not start the session");
e.Observe(10, true);
Check(e.Started() && e.Total() == 10 && e.Stages() == 10, "first response sets the total");
Check(e.Accepted() == 0 && !e.Complete(), "nothing accepted yet");
Check(e.Total() == 10 && e.Stages() == 10, "the total comes from the config");
e.Observe(9, true);
Check(e.Accepted() == 1, "rem 10 -> 9 is one accepted sample");
// rem is reported AFTER the sample is processed, so the first reading
// of a healthy enrolment is already 9 and one sample is in.
Check(e.Started() && e.Accepted() == 1, "a first reading of 9 means one accepted");
Check(!e.Complete(), "not complete");
for (std::int32_t r : {8, 7, 6, 5, 4, 3, 2, 1}) e.Observe(r, true);
Check(e.Accepted() == 9, "nine accepted at one remaining");
Check(!e.Complete() && e.Remaining() == 1, "not complete at one remaining");
e.Observe(0, true);
Check(e.Complete() && e.Accepted() == 10, "complete at zero");
@ -160,33 +162,34 @@ int main() {
// itself finished after ONE press. SAVE_DATA is then called on an
// algorithm holding no template and answers -1.
{
EnrolSession e;
e.Observe(10, true); // touch: ten to go
EnrolSession e(10);
e.Observe(9, true); // touch: one accepted, nine to go
e.Observe(0, false); // release: reads zero, means nothing
Check(!e.Complete(), "a release reading of 0 does NOT complete the enrolment");
Check(e.Remaining() == 10, "and does not move the count");
Check(e.Accepted() == 0, "nothing was accepted");
Check(e.Remaining() == 9, "and does not move the count");
Check(e.Accepted() == 1, "the touch's one sample still stands");
}
// A first reading of 0 is an unpopulated field, not a finished enrolment.
{
EnrolSession e;
EnrolSession e(10);
e.Observe(0, true);
Check(!e.Started(), "a leading zero does not start a session");
Check(!e.Complete(), "and certainly does not finish one");
e.Observe(10, true);
e.Observe(9, true);
Check(e.Started() && e.Total() == 10, "a real count still starts it");
}
// The count only falls; a jump back up is noise, not progress.
{
EnrolSession e;
e.Observe(10, true);
EnrolSession e(10);
e.Observe(9, true);
e.Observe(8, true);
e.Observe(7, true);
Check(e.Remaining() == 7, "decreases are taken");
e.Observe(9, true);
Check(e.Remaining() == 7, "an increase is ignored");
Check(e.Accepted() == 3, "three accepted, counted against the real total");
}
// ---- The three recorded runs, replayed in order