# zkteco
[](https://crates.io/crates/zkteco)
[](https://github.com/msaied/zkteco-rs)
[](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).