Native drivers are a non-goal; see ADR 0004.
This page is the seam reference: the trait, the layers, the license rule, and the
acceptance criteria. For the step-by-step bring-up (identify the device, scaffold a driver,
decode its frames, open a pull request) follow
Bringing up a sensor with fpdev. The fpdev workbench drives
every phase offline.
A native, host-image sensor is expressed as one small async trait,
FrameSource, in
fprint-backend-native:
pub trait FrameSource {
async fn capture(&mut self) -> Result<Capture>; // the one poll boundary
async fn arm(&mut self) -> Result<()> { Ok(()) } // default no-op
async fn disarm(&mut self) -> Result<()> { Ok(()) }
}Your driver's whole job is to turn hardware into a grayscale Frame. Everything
downstream is already built and verified:
ImageDevice<S: FrameSource>drives your source and is a completefprint_core::Device(enroll / verify / identify).detectorrunsfprint-mindtct(frame → minutiae) andmatcherrunsfprint-bozorth3(minutiae → score). Both are golden bit-exact.
So a new driver is just a FrameSource — you do not touch fprint-core, the daemon,
or the matcher.
Three FrameSource implementors already exist to copy from:
SyntheticFrameSourceandFileFrameSource— hardware-free.UsbFrameSource— an experimental, hardware-unverified worked example for the Validity VFS5011, layered asproto(pure framing) →transport(thenusbseam) →source(the driver) →vfs5011(device constants). Its protocol values are placeholders marked "HW-verified: required"; treat it as a shape to follow, not a working driver.
A good pattern is to keep protocol framing in pure, unit-tested Vec<u8> code and
confine unsafe/nusb I/O to the transport leaf, exactly as usb/ does.
This is the one hard rule for driver contributions (see
ARCHITECTURE.md §Provenance & licensing):
- Original code from interoperability facts is fine. VID/PID, endpoints, frame geometry, register names, and init sequences are facts, not copyrightable expression. Document them and write original Rust.
- Transliterating a libfprint driver is not. A line-by-line port of LGPL driver
code is a derivative work; it cannot be
MIT OR Apache-2.0. If you genuinely port one, it must live in a separate LGPL-2.1-or-later crate, isolated from the permissive core, carrying its own SPDX header.
#![forbid(unsafe_code)]holds, except in the transport leaf where the FFI/USB boundary genuinely needs it (quarantined, asnusbis today).- Every device value carries an
HW-verified:marker recording whether hardware has vouched for it (see below); the pending ones are the bring-up's remaining work. - Verified black-box: golden fixtures, mock-transport tests, or captured-frame
round-trips — the way
sources/andusb/mock_tests.rsare. - REUSE clean: every new file declares its license (inline SPDX for
.rs, or theREUSE.tomlbulk annotation), andmise run reusepasses. - Passes the workspace lints:
cargo clippy --workspace --all-targets -- -D warningsandcargo fmt --all --check.
A driver's device constants — VID/PID, endpoints, frame geometry, init/deinit sequences — are interoperability facts that only a physical sensor can confirm. Each one carries a marker in its doc-comment recording that state, on a single axis with two positions:
// HW-verified: required— pending. A placeholder or an unconfirmed fact that no hardware has vouched for. This is what the scaffold emits.// HW-verified: confirmed <evidence>— resolved. Checked against hardware, with<evidence>recording how (a capture, a descriptor dump, a datasheet reference).
cargo xtask hw-checklist [driver] lists the pending markers, so a bring-up can see
what is left to confirm. A bare HW-verified: required stays pending until someone
confirms the value and writes the evidence in; nothing else changes its meaning.
Open a draft PR early — we're happy to help shape the seam with you.