Add a knob: an undecided press at lift is a no-match

rescan=0 made every frame terminal, which is what stopped a wrong finger from
hanging the client -- but it also disabled the trustlet's own "image not good
enough yet, try again" answer, so a marginal first frame became a hard
rejection. In real use that showed as a quick tap of the enrolled finger
answering verify-no-match: one frame, rejected. Matches land at frame 3, 5, 8
of a press; the first frame is the finger landing.

The alternative is to let the trustlet keep its rescan budget and handle the
wrong finger where it actually manifests, at the press level: a press that
lifts with no terminal verdict is reported as no-match. The trustlet then
answers -11 on marginal frames instead of rejecting them, and the press keeps
going until it matches or the finger lifts.

A knob rather than a decision, because the trade-off is measured, not assumed:
under the stock budget seven of nineteen correct presses ended undecided at the
old 500 ms frame rate, and whether the faster loop closes that gap is the
question the next runs answer.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 22:49:59 +02:00
commit 93ab8570e0

View file

@ -81,6 +81,15 @@ std::string g_logDir = "/var/log/fingerprintd";
std::string g_stateDir = "/var/lib/fingerprintd"; std::string g_stateDir = "/var/lib/fingerprintd";
int g_rescan = -1; // -1 = leave the config's value alone int g_rescan = -1; // -1 = leave the config's value alone
// What a press that ends with no terminal verdict means. At the stock rescan
// budget a wrong finger answers "not identified yet" on every frame and never
// yields a terminal frame, so its press is undecided at lift -- and the only
// way a client ever hears verify-no-match is to treat that as one. The cost is
// on the correct finger: a press that ran out of frames before the matcher
// 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;
// The namespace key the trustlet hashes into the SFS group's directory name. // The namespace key the trustlet hashes into the SFS group's directory name.
// It defaults to Android's because that is what this device's existing store // It defaults to Android's because that is what this device's existing store
// was written under. It does NOT isolate anything -- SET_ACTIVE_GROUP's path // was written under. It does NOT isolate anything -- SET_ACTIVE_GROUP's path
@ -1297,12 +1306,13 @@ public:
inPress = false; inPress = false;
if (pressMatched) { if (pressMatched) {
out.decided = true; out.matched = true; out.fid = pressFid; out.decided = true; out.matched = true; out.fid = pressFid;
} else if (pressRejected) { } else if (pressRejected || (g_undecidedIsNoMatch && pressFrames > 0)) {
out.decided = true; out.matched = false; out.decided = true; out.matched = false;
} }
std::println(" press {}: {} frames, {} rescans -> {}", out.presses, pressFrames, std::println(" press {}: {} frames, {} rescans -> {}", out.presses, pressFrames,
rescans, pressMatched ? std::format("MATCH fid={}", pressFid) rescans, pressMatched ? std::format("MATCH fid={}", pressFid)
: pressRejected ? "NO MATCH" : "undecided"); : pressRejected ? "NO MATCH"
: out.decided ? "NO MATCH (undecided at lift)" : "undecided");
} }
} }
if (finger) pressFrames++; if (finger) pressFrames++;
@ -2060,6 +2070,7 @@ int main(int argc, char** argv) {
if (a == "--cal-save") { doCalSave = true; probe = true; g_verbose = true; } if (a == "--cal-save") { doCalSave = true; probe = true; g_verbose = true; }
if (a.starts_with("--frames=")) frames = std::stoi(std::string(a.substr(9))); if (a.starts_with("--frames=")) frames = std::stoi(std::string(a.substr(9)));
if (a.starts_with("--frame-gap=")) g_frameGapMs = std::stoi(std::string(a.substr(12))); if (a.starts_with("--frame-gap=")) g_frameGapMs = std::stoi(std::string(a.substr(12)));
if (a == "--undecided=nomatch") g_undecidedIsNoMatch = true;
if (a.starts_with("--log-dir=")) g_logDir = a.substr(10); 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("--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("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));