Run the frame loop at 40 ms, and time it

500 ms between frames was the research harness's pace, chosen so a person
could read the transcript as it scrolled. It became the daemon's pace by
inheritance, not by decision, and it is the root of both complaints from real
use: false negatives and a slow answer after lifting.

The matcher rejects the early frames of a correct press and matches several
frames in -- frames 3 and 8 in the acceptance run. So the number of frames a
press gets is what decides it, and at 500 ms a one-second press gets two. And a
lift is only noticed on the next frame, so the gap is also the latency a user
feels before verify-no-match.

A frame costs four QTEE round trips whatever the gap; the gap is pure delay on
top. 40 ms is a starting point, not a measurement: the loop now logs
milliseconds per frame so the real achievable rate is read rather than assumed,
and logs the IRQ line alongside the metric so it is visible whether gpio75
tracks the finger under an armed session -- if it does, lift detection can
become an edge wait instead of a poll. --frame-gap= overrides it.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 22:36:47 +02:00
commit 9fbf5e6c73

View file

@ -68,7 +68,14 @@ namespace {
constexpr const char* Version = "0.1.0"; constexpr const char* Version = "0.1.0";
bool g_verbose = false; bool g_verbose = false;
int g_frameGapMs = 500; // 500 ms was the research harness's pace, chosen so a human could read the
// transcript scroll by. It is not a design. The matcher rejects the early
// frames of a correct press and matches several frames in (frames 3 and 8 in
// the acceptance run), so frames-per-press is what decides a press -- and a
// lift is only noticed on the NEXT frame, so it is also the latency a user
// feels. A frame costs four QTEE round trips regardless; the gap on top is
// pure delay.
int g_frameGapMs = 40;
int g_samples = 10; // common.max_enrolling_samples, as shipped int g_samples = 10; // common.max_enrolling_samples, as shipped
std::string g_logDir = "/var/log/fingerprintd"; std::string g_logDir = "/var/log/fingerprintd";
std::string g_stateDir = "/var/lib/fingerprintd"; std::string g_stateDir = "/var/lib/fingerprintd";
@ -1242,6 +1249,11 @@ public:
bool inPress = false, pressMatched = false, pressRejected = false; bool inPress = false, pressMatched = false, pressRejected = false;
int pressFrames = 0, rescans = 0; int pressFrames = 0, rescans = 0;
std::uint32_t pressFid = 0; std::uint32_t pressFid = 0;
auto t0 = std::chrono::steady_clock::now();
auto msSince = [&](auto t) {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t).count();
};
// The reference frame loop is {QUERY, CAPTURE, REPORT, QUERY, REPORT}. // The reference frame loop is {QUERY, CAPTURE, REPORT, QUERY, REPORT}.
// The trailing query acknowledges the trustlet's event state; without // The trailing query acknowledges the trustlet's event state; without
@ -1300,13 +1312,22 @@ public:
out.decided = true; out.matched = true; out.fid = pressFid; out.decided = true; out.matched = true; out.fid = pressFid;
std::println(" press {}: {} frames -> MATCH fid={}", out.presses, pressFrames, pressFid); std::println(" press {}: {} frames -> MATCH fid={}", out.presses, pressFrames, pressFid);
} }
if (g_verbose)
std::println(" frame {:3}: metric={:<4}{}{}", i + 1, c.metric,
finger ? " FINGER" : " ", note);
SendCommand(app_, ta::Cmd::QueryEventStatus, q); SendCommand(app_, ta::Cmd::QueryEventStatus, q);
if (g_verbose) {
// The IRQ line alongside the metric: if it tracks the finger
// under an armed session, lift detection can become an edge
// wait instead of a poll.
auto irq = sensor_.ReadIrq();
std::println(" frame {:3} @{:5}ms: metric={:<4}{} irq={}{}", i + 1,
msSince(t0), c.metric, finger ? " FINGER" : " ",
irq ? std::to_string(*irq) : "?", note);
}
std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs)); std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs));
} }
fingerPresent_.store(false); fingerPresent_.store(false);
std::println(" verify loop: {} frames in {} ms ({} ms/frame incl. {} ms gap)",
out.frames, msSince(t0),
out.frames ? msSince(t0) / out.frames : 0, g_frameGapMs);
if (cancel) { if (cancel) {
SendCommand(app_, ta::Cmd::Cancel, {}); SendCommand(app_, ta::Cmd::Cancel, {});
out.cancelled = true; out.cancelled = true;
@ -2006,6 +2027,7 @@ int main(int argc, char** argv) {
if (a == "--enrol") { doEnrol = true; probe = true; } if (a == "--enrol") { doEnrol = true; probe = true; }
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("--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)));