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";