zkteco 1.0.0

A pure-Rust client for ZKTeco biometric attendance / access-control terminals (TCP/UDP protocol). A faithful port of the Python `pyzk` library.
Documentation
  • Coverage
  • 100%
    129 out of 129 items documented2 out of 2 items with examples
  • Size
  • Source code size: 140.58 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 2.11 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 5s Average build duration of successful builds.
  • all releases: 5s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • msaied/zkteco-rs
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • msaied

zkteco

crates.io docs.rs license

A pure-Rust, synchronous client for ZKTeco biometric attendance and access-control terminals — the fingerprint / RFID / face devices that speak the ZK TCP/UDP protocol on port 4370.

This crate is a faithful port of the well-known Python library pyzk. If you have used pyzk, the API will feel familiar.

Features

  • Connect & authenticate (with the scrambled comm-key when a device password is set)
  • TCP (default) and UDP transports; auto-detects the 28-byte "ZK6" and 72-byte "ZK8" user record layouts
  • Users: list, create/update, delete, clear all data
  • Attendance: download all records, clear records
  • Fingerprint templates: list, fetch one, save (single & bulk high-rate), delete, enroll
  • Live capture: stream punches in real time as an iterator
  • Device info: firmware, serial, platform, MAC, name, fingerprint/face versions, network params, capacities
  • Device control: get/set time, restart, power off, refresh, free buffer, unlock door, lock state, test voice, write/clear LCD
  • Only one dependency (chrono); #![forbid(unsafe_code)]

Install

cargo add zkteco

Quick start

use zkteco::Zk;

fn main() -> zkteco::ZkResult<()> {
    // TCP by default, port 4370.
    let mut zk = Zk::builder("192.168.1.201").build();
    zk.connect()?;

    // Best practice: lock the device during bulk reads, then unlock.
    zk.disable_device()?;
    let users = zk.get_users()?;
    let logs = zk.get_attendance()?;
    zk.enable_device()?;

    println!("{} users, {} attendance records", users.len(), logs.len());

    zk.disconnect()?;
    Ok(())
}

Configuration (builder)

use std::time::Duration;
use zkteco::Zk;

let zk = Zk::builder("192.168.1.201")
    .port(4370)                      // default 4370
    .password(0)                     // device comm password (default 0 = none)
    .timeout(Duration::from_secs(10))// socket read timeout (default 60s)
    .force_udp(false)                // default false (TCP)
    .omit_ping(true)                 // skip ICMP ping probe (useful when ICMP is blocked)
    .verbose(false)                  // print protocol diagnostics to stderr
    .build();

Usage

Device info & capacities

# use zkteco::Zk;
# fn main() -> zkteco::ZkResult<()> {
# let mut zk = Zk::builder("192.168.1.201").build();
# zk.connect()?;
println!("firmware: {}", zk.get_firmware_version()?);
println!("serial:   {}", zk.get_serialnumber()?);
println!("time:     {}", zk.get_time()?);

let sizes = zk.read_sizes()?;
println!("{}/{} users, {}/{} records",
    sizes.users, sizes.users_cap, sizes.records, sizes.rec_cap);
# Ok(()) }

Create & delete a user

# use zkteco::{User, Zk, USER_DEFAULT};
# fn main() -> zkteco::ZkResult<()> {
# let mut zk = Zk::builder("192.168.1.201").build();
# zk.connect()?;
// Read existing users first so uid/user_id counters are populated.
let _ = zk.get_users()?;

// uid = 0 → the crate assigns the next free uid / user_id.
let user = User::new(0, "Alice", USER_DEFAULT, "1234", "", "", 0);
zk.set_user(&user)?;

// Delete by user_id (uid = 0 triggers a lookup).
zk.delete_user(0, "1001")?;
# Ok(()) }

Fingerprint templates

# use zkteco::Zk;
# fn main() -> zkteco::ZkResult<()> {
# let mut zk = Zk::builder("192.168.1.201").build();
# zk.connect()?;
// All templates on the device:
let templates = zk.get_templates()?;

// One template for (uid, finger_id):
if let Some(finger) = zk.get_user_template(1, 0)? {
    let bytes = finger.repack(); // serialize to store/restore later
    println!("template {} bytes", bytes.len());
}

// Interactive enrollment (the user presses their finger on the device):
let ok = zk.enroll_user(1, 0, "")?;
println!("enrolled: {ok}");
# Ok(()) }

Live capture

use std::time::Duration;
use zkteco::Zk;

# fn main() -> zkteco::ZkResult<()> {
let mut zk = Zk::builder("192.168.1.201").build();
zk.connect()?;

let mut capture = zk.live_capture(Duration::from_secs(10))?;
// Clone this handle to another thread to stop capture from outside.
let _stop = capture.stop_handle();

for event in &mut capture {
    match event? {
        Some(att) => println!("punch: {att}"),
        None => { /* timeout tick: check your own stop condition here */ }
    }
}
# Ok(()) }

Device control

# use chrono::Local;
# use zkteco::Zk;
# fn main() -> zkteco::ZkResult<()> {
# let mut zk = Zk::builder("192.168.1.201").build();
# zk.connect()?;
zk.set_time(Local::now().naive_local())?; // sync the clock
zk.unlock(3)?;                              // open the door for 3 seconds
zk.test_voice(0)?;                          // play "Thank you"
zk.write_lcd(1, "Hello")?;
// zk.restart()?; zk.poweroff()?;           // (these end the session)
# Ok(()) }

Using from async code (tokio, etc.)

Every method blocks. From an async runtime, run the work on a blocking task:

# use zkteco::{User, Zk};
# async fn demo() -> zkteco::ZkResult<()> {
let users = tokio::task::spawn_blocking(|| -> zkteco::ZkResult<Vec<User>> {
    let mut zk = Zk::builder("192.168.1.201").build();
    zk.connect()?;
    let users = zk.get_users()?;
    zk.disconnect()?;
    Ok(users)
})
.await
.expect("task panicked")?;
# Ok(()) }

Error handling

All fallible calls return ZkResult<T> = Result<T, ZkError>. ZkError distinguishes NotConnected, Unauthenticated, Response (device returned a failure code), NotSupported, Network (socket/timeout), and Parse (malformed reply). It implements std::error::Error, so it works with ?, anyhow, thiserror, etc.

Encoding

User names and passwords are encoded/decoded as UTF-8 (lossy). Legacy code-page encodings (e.g. GBK) are intentionally not supported to keep the crate dependency-light.

Timestamps

ZK devices have no timezone concept; times are returned as chrono::NaiveDateTime in the device's local time.

Troubleshooting

  • Can't connect / hangs on connect: many networks block ICMP. Pass .omit_ping(true) so the crate skips the ping reachability probe.
  • Timeouts: increase .timeout(...). Large attendance/template downloads can take a while.
  • Wrong/garbled user names: check the device's character encoding — only UTF-8 is supported here.
  • Unauthenticated: set the correct device communication password with .password(...).
  • UDP-only devices: pass .force_udp(true).

Compatibility

Tested in spirit against the same device families pyzk targets (ZK6/ZK8 firmware generations) over both TCP and UDP. Behavior is a direct translation of pyzk's proven protocol handling. If you hit a device quirk that pyzk handles differently, please open an issue.

Acknowledgements

This is a port of pyzk by Fanani M. Ihsan and contributors. All protocol knowledge is derived from that project.

License

Licensed under the GNU General Public License v2.0 or later (GPL-2.0-or-later). Because this is a derivative of the GPL-licensed pyzk, the port inherits the same license. See LICENSE.