From ae2f537a40ecde1233a1009e4d2a0bb19866c4b6 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 5 Sep 2026 02:00:39 +0200 Subject: [PATCH] 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. --- implementations/main.cpp | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index e3b735d..336c33a 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -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 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";