Answer a failing press while the finger is still on the sensor

Jorijn asked whether the timeouts were the wrong-finger path or something to do
with learning. Neither. It is a deadlock between the press rule and the
instruction the user is given.

A press was judged only when the finger was released. The user is told to hold
until it answers. So a press whose frames keep rejecting is never released, the
daemon never judges it, and the client sits there until its own timeout expires.
The transcript shows exactly that: twenty-nine consecutive full-contact frames
rejected across twenty seconds with the finger down throughout, surfaced to the
user as a hang rather than as a failure.

Detection was fine, the settled gate was fine, the matcher was doing its job and
saying no. Nothing was ever going to tell the user so.

A press now answers no-match once it has accumulated five rejected frames,
without waiting for a release. At roughly seven hundred milliseconds a frame
that is about three and a half seconds -- long enough that the frame-3 and
frame-8 matches in this project's records still land, short enough to be an
answer instead of a wait. A phone tells you it did not recognise you while your
finger is still on it.
This commit is contained in:
Jorijn van der Graaf 2026-09-05 02:00:39 +02:00
commit ae2f537a40

View file

@ -98,6 +98,21 @@ int g_rescan = -1; // -1 = leave the config's value alone
// reached a verdict is also reported as no-match. Under rescan=0 this cannot
// arise (every frame is terminal), so the knob only matters with a budget.
bool g_undecidedIsNoMatch = false;
// How many rejected frames one press may accumulate before the daemon calls it
// and answers no-match, WITHOUT waiting for the finger to lift.
//
// Judging a press only at release deadlocks against the instruction the user is
// given. Told to hold until it answers, a user holds; a press whose frames keep
// rejecting is never released, so the daemon never judges it and the client sits
// there until its own timeout. Measured 2026-09-05: 29 consecutive full-contact
// frames rejected over 20 seconds with the finger down the whole time, reported
// to the user as a hang rather than a failure.
//
// A phone tells you it did not recognise you while your finger is still on it.
// At ~700 ms a frame, five is about three and a half seconds -- long enough that
// the frame-3 and frame-8 matches in this project's records still land, short
// enough to be an answer rather than a wait.
int g_pressRejectBudget = 5;
bool g_irqObserve = false;
bool g_edgeWake = false;
@ -1447,7 +1462,7 @@ public:
en::TouchTracker tracker;
harvested_ = 0; // no fold has happened in this session yet
bool inPress = false, pressMatched = false, pressRejected = false;
int pressFrames = 0, rescans = 0, skipped = 0;
int pressFrames = 0, rescans = 0, skipped = 0, pressRejects = 0;
std::uint32_t pressFid = 0;
auto t0 = std::chrono::steady_clock::now();
// When contact first appeared. The wall clock a client sees starts when
@ -1491,7 +1506,7 @@ public:
for (ta::Event ev : tracker.Observe(finger, settled, en::Mode::Authenticate)) {
if (ev == ta::Event::FingerTouched) {
inPress = true; pressMatched = false; pressRejected = false;
pressFrames = 0; rescans = 0; pressFid = 0;
pressFrames = 0; rescans = 0; pressFid = 0; pressRejects = 0;
out.presses++;
}
std::vector<std::byte> evbuf(ta::EventContextSize);
@ -1527,7 +1542,19 @@ public:
note += " folded";
}
break;
case ta::Verdict::Rejected: pressRejected = true; note += " rej"; break;
case ta::Verdict::Rejected:
pressRejected = true; ++pressRejects; note += " rej";
// Answer while the finger is still down. See
// g_pressRejectBudget: waiting for a release the user has
// been told not to make is a deadlock, not a policy.
if (!pressMatched && g_pressRejectBudget > 0
&& pressRejects >= g_pressRejectBudget) {
out.decided = true; out.matched = false;
std::println(" press {}: {} rejected frame(s) -> NO MATCH "
"(budget reached, finger still down)",
out.presses, pressRejects);
}
break;
case ta::Verdict::NotIdentifiedYet: rescans++; note += " -11"; break;
case ta::Verdict::MatcherNeverRan: break;
}
@ -2610,6 +2637,7 @@ int main(int argc, char** argv) {
if (a.starts_with("--log-dir=")) g_logDir = a.substr(10);
if (a.starts_with("--state-dir=")) g_stateDir = a.substr(12);
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
if (a.starts_with("--reject-budget=")) g_pressRejectBudget = std::stoi(std::string(a.substr(16)));
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))); g_samplesForced = true; }
if (a.starts_with("--learn=")) g_learn = a.substr(8) != "0";