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
# zkteco

[![crates.io](https://img.shields.io/crates/v/zkteco.svg)](https://crates.io/crates/zkteco)
[![docs.rs](https://docs.rs/zkteco/badge.svg)](https://github.com/msaied/zkteco-rs)
[![license](https://img.shields.io/crates/l/zkteco.svg)](https://github.com/msaied/zkteco-rs/blob/main/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`](https://github.com/fananimi/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

```sh
cargo add zkteco
```

## Quick start

```rust,no_run
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)

```rust,no_run
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

```rust,no_run
# 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

```rust,no_run
# 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

```rust,no_run
# 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

```rust,no_run
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

```rust,no_run
# 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:

```rust,no_run
# 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>`](https://docs.rs/zkteco/latest/zkteco/type.ZkResult.html)
= `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`](https://github.com/fananimi/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](LICENSE).