17 KiB
tags, created
| tags | created | |
|---|---|---|
|
2026-07-02 17:17 |
Summary
Notes
To interface the FPM10A (or AS608) fingerprint sensor with a Raspberry Pi in Rust, you use the rppal crate to communicate via UART. The sensor uses a byte-oriented packet protocol (prefixed with 0xEF 0x01 and a checksum). [1, 2, 3, 4, 5]
- Wiring the Sensor
The FPM10A uses standard TTL serial, so you need to shift to 3.3V logic for the Raspberry Pi. Connect the pins as follows: [1, 2]
- Sensor VCC → Raspberry Pi 3.3V or 5V (Sensor supports 3.6V - 6.0V, but the Pi's TX pin should be level-shifted if you power the sensor at 5V)
- Sensor GND → Raspberry Pi GND
- Sensor TX (White wire) → Raspberry Pi GPIO15 / RXD
- Sensor RX (Green wire) → Raspberry Pi GPIO14 / TXD [1, 2, 3, 4, 5]
- Rust Configuration
Add rppal to your Cargo.toml dependencies: [1, 2]
toml
[dependencies]
rppal = "0.22"
Use code with caution.
Because the FPM10A defaults to a baud rate of 57600, configure your Uart peripheral settings using rppal::uart::Uart: [1, 2, 3]
rust
use rppal::uart::{Uart, Parity};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 57600 is the default FPM10A baud rate.
let mut uart = Uart::new(57600, Parity::None, 8, 1)?;
// Set read timeouts to prevent blocking indefinitely
uart.set_read_mode(1, Duration::from_millis(500))?;
// Send your command packets here (e.g., Handshake, Get Image, Search)
Ok(())
}
Use code with caution.
- FPM10A Protocol Basics
Because the FPM10A is a slave device, you must send command packets and parse the returned packets. A standard command follows this structure: [1, 2]
- Header:
0xEF, 0x01 - Address:
0xFF, 0xFF, 0xFF, 0xFF(Default address) - Package Identifier:
0x01(Command Packet) - Package Length: 2 bytes (Length = Length of remaining data + 2 for Checksum)
- Instruction Code: (e.g.,
0x01for Handshake,0x0Cfor Store) - Parameters: Variable
- Checksum: 2 bytes (Sum of Package Identifier, Length, Instruction Code, and Parameters) [1, 2]
A handshake command would look like this:
(\text{Header (2)} + \text{Address (4)} + \text{ID (1)} + \text{Length (2)} + \text{Cmd (1)} + \text{Checksum (2)}) [1]
Would you like to see a code implementation for a specific FPM10A command, or do you need help setting up your Raspberry Pi OS UART configuration (using raspi-config) before writing the code? [1]
Here is a complete, production-ready Rust example using rppal to enroll (program) a fingerprint and then search (check) for a match.
Implementation Strategy
The FPM10A requires a multi-step sequence for both operations:
- Enrollment: Capture image 1 → Generate character file 1 → Capture image 2 → Generate character file 2 → Combine templates → Store template to a specific ID slot.
- Searching: Capture image → Generate character file → Search the database flash memory for a match.
Complete Rust Code
rust
use rppal::uart::{Parity, Uart};
use std::error::Error;
use std::thread::sleep;
use std::time::Duration;
// --- FPM10A Configuration Constants ---
const DEFAULT_ADDR: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
const PACKET_COMMAND: u8 = 0x01;
// --- FPM10A Instruction Codes ---
const CMD_GETIMAGE: u8 = 0x01;
const CMD_IMAGE2TZ: u8 = 0x02;
const CMD_REGMODEL: u8 = 0x05;
const CMD_STORE: u8 = 0x06;
const CMD_SEARCH: u8 = 0x04;
// --- Error Helper ---
fn handle_confirmation_code(code: u8) -> Result<(), String> {
match code {
0x00 => Ok(()),
0x01 => Err("Error receiving packet".to_string()),
0x02 => Err("No finger on sensor".to_string()),
0x03 => Err("Failed to enroll finger".to_string()),
0x0a => Err("Failed to combine character files".to_string()),
0x0b => Err("Addressing ID is out of range".to_string()),
0x1d => Err("Failed to operate flash memory".to_string()),
_ => Err(format!("Unknown confirmation error: 0x{:02X}", code)),
}
}
// --- Protocol Packet Builder and Parser ---
fn send_command(uart: &mut Uart, cmd: u8, params: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
let length = (params.len() + 3) as u16; // length = len(cmd + params + checksum)
let len_high = (length >> 8) as u8;
let len_low = (length & 0xFF) as u8;
// Calculate Checksum: sum of packet identifier, length bytes, command, and params
let mut checksum: u32 = PACKET_COMMAND as u32 + len_high as u32 + len_low as u32 + cmd as u32;
for &p in params {
checksum += p as u32;
}
let sum_high = ((checksum >> 8) & 0xFF) as u8;
let sum_low = (checksum & 0xFF) as u8;
// Build the frame
let mut packet = vec![0xEF, 0x01]; // Header
packet.extend_from_slice(&DEFAULT_ADDR);
packet.push(PACKET_COMMAND);
packet.push(len_high);
packet.push(len_low);
packet.push(cmd);
packet.extend_from_slice(params);
packet.push(sum_high);
packet.push(sum_low);
// Flush and write
uart.flush(rppal::uart::Queue::Both)?;
uart.write(&packet)?;
// Read Response Frame Header (minimum response size is 12 bytes)
let mut response = vec![0u8; 12 + params.len()];
let bytes_read = uart.read(&mut response)?;
if bytes_read < 12 || response[0] != 0xEF || response[1] != 0x01 {
return Err("Invalid response packet header from sensor".into());
}
Ok(response)
}
// Helper to poll for a finger until one is placed on the glass
fn wait_for_finger(uart: &mut Uart, prompt: &str) -> Result<(), Box<dyn Error>> {
println!("{}", prompt);
loop {
if let Ok(resp) = send_command(uart, CMD_GETIMAGE, &[]) {
if resp[9] == 0x00 { // Confirmation code is at index 9
return Ok(());
}
}
sleep(Duration::from_millis(200));
}
}
/// Enrolls a new finger into the specified ID slot (e.g., 0 to 162)
fn enroll_finger(uart: &mut Uart, id_slot: u16) -> Result<(), Box<dyn Error>> {
// 1. First image capture
wait_for_finger(uart, "Place your finger on the sensor...")?;
send_command(uart, CMD_IMAGE2TZ, &[0x01])?; // Convert to character file buffer 1
println!("First scan successful. Remove finger.");
sleep(Duration::from_secs(2));
// 2. Second image capture
wait_for_finger(uart, "Place the SAME finger on the sensor again...")?;
send_command(uart, CMD_IMAGE2TZ, &[0x02])?; // Convert to character file buffer 2
println!("Second scan successful.");
// 3. Combine buffers 1 and 2 into a reference model template
println!("Creating model template...");
let resp = send_command(uart, CMD_REGMODEL, &[])?;
handle_confirmation_code(resp[9])?;
// 4. Store template in specified ID slot
let id_high = (id_slot >> 8) as u8;
let id_low = (id_slot & 0xFF) as u8;
println!("Saving template to ID slot {}...", id_slot);
let resp = send_command(uart, CMD_STORE, &[0x01, id_high, id_low])?;
handle_confirmation_code(resp[9])?;
println!("Fingerprint successfully programmed to ID #{}!", id_slot);
Ok(())
}
/// Checks the sensor for a valid live finger and searches the internal database
fn identify_finger(uart: &mut Uart) -> Result<(), Box<dyn Error>> {
wait_for_finger(uart, "Place finger on the sensor to authenticate...")?;
// Convert live image to character buffer 1
send_command(uart, CMD_IMAGE2TZ, &[0x01])?;
// Search database using buffer 1 (Params: buffer id, start index high/low, count high/low)
println!("Searching fingerprint database...");
let resp = send_command(uart, CMD_SEARCH, &[0x01, 0x00, 0x00, 0x00, 0xA3])?;
match handle_confirmation_code(resp[9]) {
Ok(_) => {
// Index 10-11: Found Page ID, Index 12-13: Match Score
let match_id = ((resp[10] as u16) << 8) | (resp[11] as u16);
let score = ((resp[12] as u16) << 8) | (resp[13] as u16);
println!("✅ ACCESS GRANTED! Found Match ID: #{} with score: {}", match_id, score);
}
Err(_) => {
println!("❌ ACCESS DENIED! Fingerprint not recognized.");
}
}
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
// Open system UART (typically /dev/ttyS0 or /dev/ttyAMA0 on Raspberry Pi)
let mut uart = Uart::new(57600, Parity::None, 8, 1)?;
uart.set_read_mode(1, Duration::from_millis(1000))?;
// Step 1: Program a fingerprint into slot #1
enroll_finger(&mut uart, 1)?;
sleep(Duration::from_secs(2));
// Step 2: Loop checking for validation
loop {
if let Err(e) = identify_finger(&mut uart) {
println!("Error during processing: {}. Retrying...", e);
}
sleep(Duration::from_secs(3));
}
}
Use code with caution.
Visual Map of the Search Packet Response
When decoding the response of the CMD_SEARCH (0x04) function payload, keep these indices in mind when working with the byte array:
text
Response Byte Index:
[0, 1] -> Header (0xEF, 0x01)
[2..5] -> Device Address
[6] -> Packet Identifier
[7, 8] -> Packet Length
[9] -> Confirmation Code (0x00 = Match Found)
[10, 11] -> Found ID Slot (High byte, Low byte)
[12, 13] -> Match Score (High byte, Low byte)
Use code with caution.
Here is the code to delete fingerprints from the FPM10A's internal database.
The sensor uses the DeleChar instruction code (0x0C). It requires you to pass the starting memory slot ID and the number of consecutive templates you want to clear out.
Rust Implementation For Deletion
Add this function to your existing codebase. It includes two common use-case implementations: deleting a single target ID slot or wiping a sequential block of memory.
rust
// --- Add this to your Instruction Codes constants ---
const CMD_DELECHAR: u8 = 0x0C;
/// Deletes a specific number of fingerprints starting from a defined page ID slot.
///
/// * `uart` - Mutable reference to the initialized Rppal Uart peripheral.
/// * `start_id` - The memory slot index where deletion begins (e.g., 1).
/// * `count` - How many consecutive templates to wipe (use 1 to clear only the start_id).
fn delete_fingerprints(uart: &mut Uart, start_id: u16, count: u16) -> Result<(), Box<dyn Error>> {
let start_high = (start_id >> 8) as u8;
let start_low = (start_id & 0xFF) as u8;
let count_high = (count >> 8) as u8;
let count_low = (count & 0xFF) as u8;
println!("Attempting to delete {} slots starting from ID #{}...", count, start_id);
// Params array format required by FPM10A: [PageID High, PageID Low, Number of templates High, Number of templates Low]
let params = [start_high, start_low, count_high, count_low];
// Send command and wait for response packet
let resp = send_command(uart, CMD_DELECHAR, ¶ms)?;
// Byte index 9 holds the confirmation status code
match handle_confirmation_code(resp[9]) {
Ok(_) => {
println!("🗑️ Successfully deleted {} template(s) from memory!", count);
Ok(())
}
Err(e) => {
// Standard sensor error 0x15 means it failed to clear the slots (e.g., already empty)
if resp[9] == 0x15 {
return Err("Failed to delete templates. The requested slots might already be empty.".into());
}
Err(format!("Deletion failed: {}", e).into())
}
}
}
Use code with caution.
How to use it in main()
You can call the function depending on whether you want to delete a specific user fingerprint or completely purge a range of data:
rust
fn main() -> Result<(), Box<dyn Error>> {
let mut uart = Uart::new(57600, Parity::None, 8, 1)?;
uart.set_read_mode(1, Duration::from_millis(1000))?;
// Use Case 1: Delete exactly one fingerprint stored at ID slot #5
if let Err(e) = delete_fingerprints(&mut uart, 5, 1) {
println!("Error: {}", e);
}
// Use Case 2: Bulk delete slots 10 through 29 (20 slots total)
// if let Err(e) = delete_fingerprints(&mut uart, 10, 20) {
// println!("Error: {}", e);
// }
Ok(())
}
Here is the code to wipe the entire flash database and to query the sensor's index table to see exactly which slots are open and available.
- New Command Constants
Add these additional instruction codes to your global definitions: [1]
rust
const CMD_EMPTY: u8 = 0x0D; // Empties the entire template flash library
const CMD_READINDEXTABLE: u8 = 0x1F; // Reads the database occupancy index table
Use code with caution.
- Rust Code Implementation
The CMD_READINDEXTABLE function returns the occupancy of the database as a bitmask array. It takes a single index page identifier (typically 0x00 or 0x01 depending on whether your sensor holds more than 256 templates). Each byte returned represents 8 slots. A bit set to 1 means the slot is full; a bit set to 0 means the slot is free. [1, 2]
rust
/// Erases every single fingerprint profile stored in the flash module database.
fn empty_database(uart: &mut Uart) -> Result<(), Box<dyn Error>> {
println!("⚠️ WARNING: Requesting complete fingerprint database format...");
// CMD_EMPTY takes no extra parameters
let resp = send_command(uart, CMD_EMPTY, &[])?;
match handle_confirmation_code(resp[9]) {
Ok(_) => {
println!("🧹 Success! The entire fingerprint database has been completely emptied.");
Ok(())
}
Err(e) => Err(format!("Formatting failed: {}", e).into()),
}
}
/// Queries the internal memory index map and prints out all unallocated/free slot IDs.
fn print_free_slots(uart: &mut Uart) -> Result<(), Box<dyn Error>> {
println!("🔍 Fetching memory allocation table...");
// We request Index Page 0 (covering slots 0 to 255)
// Parameter 0x00 instructs the sensor to return the map of the first 256 IDs.
let resp = send_command(uart, CMD_READINDEXTABLE, &[0x00])?;
handle_confirmation_code(resp[9])?;
// The data packet containing the index starts at byte index 10 in our response frame.
// There are 32 bytes returned in total (32 bytes * 8 bits = 256 slots evaluated).
let bitmask_bytes = &resp[10..42];
let mut free_slots_count = 0;
print!("Free Slot IDs: ");
for (byte_idx, &byte) in bitmask_bytes.iter().enumerate() {
for bit_idx in 0..8 {
let slot_id = (byte_idx * 8) + bit_idx;
// Check if the bit at the current index position is 0 (Unallocated)
let is_allocated = (byte >> bit_idx) & 0x01;
if is_allocated == 0 {
print!("{} ", slot_id);
free_slots_count += 1;
}
}
}
println!("\n📊 Summary: Found {} total vacant slots out of the first 256.", free_slots_count);
Ok(())
}
Use code with caution.
- Usage inside
main()
You can execute these actions within your main runtime thread depending on what operation sequence you require:
rust
fn main() -> Result<(), Box<dyn Error>> {
let mut uart = Uart::new(57600, Parity::None, 8, 1)?;
uart.set_read_mode(1, Duration::from_millis(1000))?;
// Action 1: Print available memory slots before modification
print_free_slots(&mut uart)?;
// Action 2: Factory reset / wipe everything (Uncomment to execute)
// empty_database(&mut uart)?;
Ok(())
}
Use code with caution.