Skip to content

Latest commit

 

History

60 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZKTeco ADMS

Go Reference Test codecov Go Report Card

A Go library implementing the ZKTeco ADMS protocol for ZKTeco biometric attendance devices.

Overview

This library provides a complete implementation of the HTTP-based ADMS protocol used by ZKTeco devices to communicate with servers. It handles device registration, attendance data collection, and remote command execution.

Zero external dependencies — pure Go standard library.

Features

  • Full ADMS Protocol Support: Implements all standard endpoints (/iclock/cdata, /iclock/getrequest, /iclock/devicecmd) plus device registry and inspection endpoints
  • Functional Options API: Clean, extensible configuration via WithX option functions
  • Structured Logging: Uses log/slog for structured, leveled logging
  • Context Support: Callbacks receive a context.Context tied to the server lifecycle
  • Attendance Data Processing: Parses and processes attendance logs with multiple timestamp formats
  • Device Management: Thread-safe device registration and tracking
  • Command Queuing: Queue and send commands to devices remotely
  • Heartbeat & Online Status: Tracks last activity per device with configurable online threshold
  • Concurrent-Safe: Built with goroutine-safe data structures and async callback dispatch
  • Request Body Limits: Configurable MaxBytesReader protection against oversized payloads
  • Serial Number Validation: Rejects malformed device identifiers at the protocol boundary
  • Device & Command Limits: Configurable caps on registered devices and per-device command queue depth
  • Opt-In Debug Endpoint: /iclock/inspect is disabled by default, enabled via WithEnableInspect()
  • Graceful Shutdown: Close() drains pending callbacks and cancels the base context

Installation

Requires Go 1.26.

go get github.com/s0x90/zkteco-adms

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"

    zkadms "github.com/s0x90/zkteco-adms"
)

func main() {
    server := zkadms.NewADMSServer(
        zkadms.WithOnAttendance(func(ctx context.Context, record zkadms.AttendanceRecord) {
            fmt.Printf("User %s status: %s at %s from device %s\n",
                record.UserID,
                statusString(record.Status),
                record.Timestamp.Format(time.RFC3339),
                record.SerialNumber)
        }),
        zkadms.WithOnDeviceInfo(func(ctx context.Context, sn string, info map[string]string) {
            fmt.Printf("Device %s connected: %v\n", sn, info)
        }),
    )
    defer server.Close()

    http.Handle("/iclock/", server)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func statusString(status int) string {
    switch status {
    case 0:
        return "check-in"
    case 1:
        return "check-out"
    case 2:
        return "break-out"
    case 3:
        return "break-in"
    case 4:
        return "overtime-in"
    case 5:
        return "overtime-out"
    default:
        return "unknown"
    }
}

Usage

Creating a Server

// Defaults: slog.Default() logger, 10 MB body limit, 256 callback buffer,
// 2-minute online threshold, 1-second dispatch timeout, 1000 max devices,
// 24h device eviction timeout.
server := zkadms.NewADMSServer()
defer server.Close()

Functional Options

Configure the server at construction time:

server := zkadms.NewADMSServer(
    zkadms.WithLogger(slog.New(slog.NewJSONHandler(os.Stderr, nil))),
    zkadms.WithMaxBodySize(5 << 20),              // 5 MB body limit
    zkadms.WithCallbackBufferSize(512),            // larger callback buffer
    zkadms.WithOnlineThreshold(5 * time.Minute),   // 5 min before "offline"
    zkadms.WithDispatchTimeout(2 * time.Second),   // callback dispatch timeout
    zkadms.WithBaseContext(ctx),                    // tie to parent context
    zkadms.WithOnAttendance(handleAttendance),
    zkadms.WithOnDeviceInfo(handleDeviceInfo),
    zkadms.WithOnRegistry(handleRegistry),
)
defer server.Close()
Option Default Description
WithLogger slog.Default() Structured logger
WithMaxBodySize 10 MB Max request body size
WithCallbackBufferSize 256 Internal callback channel capacity
WithOnlineThreshold 2 min Duration before a device is considered offline
WithDispatchTimeout 1 sec Max block time when callback queue is full
WithBaseContext context.Background() Parent context for callbacks
WithMaxDevices 1000 Max registered devices; use WithUnlimitedDevices() to remove the cap
WithUnlimitedDevices Remove the device registration limit (use with caution)
WithMaxCommandsPerDevice 0 (unlimited) Max queued commands per device; returns ErrCommandQueueFull
WithDeviceEvictionInterval 5 min How often the eviction worker checks for stale devices
WithDeviceEvictionTimeout 24 hours Inactivity duration before a device is automatically evicted
WithEnableInspect disabled Enable the /iclock/inspect debug endpoint
WithDefaultTimezone time.UTC Fallback timezone for parsing attendance timestamps (see WithDeviceTimezone)
WithOnAttendance nil Attendance record callback
WithOnDeviceInfo nil Device info callback
WithOnRegistry nil Device registry callback
WithOnCommandResult nil Command confirmation callback (see CommandResult)
WithOnQueryUsers nil User query callback; receives []UserRecord pushed by the device

Handling Attendance Records

zkadms.WithOnAttendance(func(ctx context.Context, record zkadms.AttendanceRecord) {
    // record.UserID       - Employee ID
    // record.Timestamp    - Time of attendance
    // record.Status       - 0=Check In, 1=Check Out, 2=Break Out, 3=Break In, 4=Overtime In, 5=Overtime Out
    // record.VerifyMode   - Verification method; use zkadms.VerifyModeName(record.VerifyMode) for label
    // record.WorkCode     - Optional work code
    // record.SerialNumber - Device serial number
    //
    // ctx is cancelled when server.Close() is called.
})

Handling Device Information

zkadms.WithOnDeviceInfo(func(ctx context.Context, sn string, info map[string]string) {
    // sn   - Device serial number
    // info - Map of device properties (firmware version, device name, etc.)
})

Handling Device Registry

zkadms.WithOnRegistry(func(ctx context.Context, sn string, info map[string]string) {
    // Called when a device registers or re-registers.
    // info contains parsed key=value pairs from the registry body.
})

Handling Command Results

zkadms.WithOnCommandResult(func(ctx context.Context, result zkadms.CommandResult) {
    // Called when a device reports the result of a command.
    // result.SerialNumber - Device that executed the command
    // result.ID           - Command ID assigned by the server
    // result.ReturnCode   - 0 = success, non-zero = error
    // result.Command      - Command type echoed back (e.g. "INFO", "DATA")
})

Handling User Query Results

zkadms.WithOnQueryUsers(func(ctx context.Context, sn string, users []zkadms.UserRecord) {
    // Called when a device pushes user records in response to
    // SendQueryUsersCommand(). Data arrives via POST /iclock/cdata.
    for _, u := range users {
        fmt.Printf("PIN=%s Name=%s Privilege=%d Card=%s\n",
            u.PIN, u.Name, u.Privilege, u.Card)
    }
})

Sending Commands to Devices

// Queue a custom command (returns ErrCommandQueueFull if limit reached)
_, err := server.QueueCommand("DEVICE001", "CHECK")

// Request device information
_, err = server.SendInfoCommand("DEVICE001")

// Heartbeat / connectivity check
_, err = server.SendCheckCommand("DEVICE001")

// Add or update a user on the device
_, err = server.SendUserAddCommand("DEVICE001", "12345", "John Doe", 0, "")

// Delete a user from the device
_, err = server.SendUserDeleteCommand("DEVICE001", "12345")

// Retrieve a device option (value arrives via device info push)
_, err = server.SendGetOptionCommand("DEVICE001", "DeviceName")

// Query all users (data pushed via POST /iclock/cdata with table=USERINFO)
_, err = server.SendQueryUsersCommand("DEVICE001")

// Execute a shell command on the device (use with caution!)
_, err = server.SendShellCommand("DEVICE001", "date")

// Request log data
_, err = server.SendLogCommand("DEVICE001")

// Drain all pending commands for a device
cmds := server.DrainCommands("DEVICE001")
for _, cmd := range cmds {
    fmt.Println(cmd.ID, cmd.Command)
}

Listing Connected Devices

devices := server.ListDevices()
for _, device := range devices {
    online := server.IsDeviceOnline(device.SerialNumber)
    fmt.Printf("Device: %s, Last Seen: %s, Online: %t\n",
        device.SerialNumber,
        device.LastActivity.Format(time.RFC3339),
        online)
}

HTTP Routing

The server implements http.Handler, so you can use it directly:

http.Handle("/iclock/", server)

Or register individual endpoints:

http.HandleFunc("/iclock/cdata", server.HandleCData)
http.HandleFunc("/iclock/registry", server.HandleRegistry)
http.HandleFunc("/iclock/getrequest", server.HandleGetRequest)
http.HandleFunc("/iclock/devicecmd", server.HandleDeviceCmd)
http.HandleFunc("/iclock/inspect", server.HandleInspect) // opt-in: not routed by ServeHTTP unless WithEnableInspect is set

Sentinel Errors

var (
    zkadms.ErrServerClosed         // operation attempted on a closed server
    zkadms.ErrCallbackQueueFull    // callback queue full, dispatch timed out
    zkadms.ErrMaxDevicesReached    // device limit reached (WithMaxDevices)
    zkadms.ErrCommandQueueFull     // per-device command queue full (WithMaxCommandsPerDevice)
    zkadms.ErrInvalidSerialNumber  // serial number failed validation
    zkadms.ErrDeviceNotFound       // operation targets an unregistered device
    zkadms.ErrInvalidCommandField  // command field contains forbidden control characters
)

Graceful Shutdown

server := zkadms.NewADMSServer(/* ... */)
// ... use server ...

// Close drains pending callbacks, cancels the base context, and stops the worker.
// Safe to call multiple times.
server.Close()

Protocol Details

ADMS Protocol Endpoints

Endpoint Method Purpose
/iclock/cdata GET/POST Receives attendance logs (ATTLOG) and operation logs (OPERLOG). Also accepts device info POSTs
/iclock/registry GET/POST Device registration and capability payloads (key=value comma-separated)
/iclock/getrequest GET Device polls for pending commands
/iclock/devicecmd POST Device reports command execution results
/iclock/inspect GET Returns JSON summary of devices and their current status (opt-in via WithEnableInspect)

Command Wire Format

When a device polls /iclock/getrequest, pending commands are sent as:

C:<ID>:<CMD>\n

Where <ID> is a monotonically increasing integer assigned by the server. For example:

C:1:INFO
C:2:DATA UPDATE USERINFO PIN=1001	Name=John Doe	Privilege=0	Card=12345678
C:3:DATA DELETE USERINFO PIN=1001

After executing a command, the device POSTs the result to /iclock/devicecmd with a body like:

ID=1&Return=0&CMD=INFO

A Return value of 0 indicates success. The parsed result is delivered to the callback registered via WithOnCommandResult.

Devices may batch multiple confirmations in a single POST:

ID=1&Return=0&CMD=DATA
ID=2&Return=0&CMD=DATA

The parser handles both batched and multiline formats (e.g. Shell command responses with Content= fields).

Command Return Codes

These error codes have been confirmed on real ZAM180-NF firmware:

Code Meaning
0 Success
-1 Command not supported or no data available
-2 File operation failed
-1002 Invalid command syntax
-1004 Table/feature not supported on this device model

Confirmed Commands

The following commands have been verified on real hardware (SpeedFace-V5L-RFID, ZAM180-NF-Ver1.1.17 firmware):

Command Convenience Method Confirmed CMD echo Notes
INFO SendInfoCommand INFO Returns full device info
CHECK SendCheckCommand CHECK Heartbeat/ping
GET OPTION FROM <key> SendGetOptionCommand GET OPTION See key list below
DATA UPDATE USERINFO PIN=... SendUserAddCommand DATA Tab-separated fields
DATA DELETE USERINFO PIN=... SendUserDeleteCommand DATA
DATA QUERY USERINFO SendQueryUsersCommand DATA Data pushed via /iclock/cdata (see note)
DATA QUERY USERINFO PIN=<n> QueueCommand DATA Query single user (see note)
Shell <cmd> SendShellCommand Shell Executes OS commands
LOG SendLogCommand LOG Request log data

Confirmed GET OPTION keys: DeviceName, FWVersion, IPAddress, MACAddress, Platform, WorkCode, LockCount, UserCount, FPCount, AttLogCount, FaceCount, TransactionCount, MaxUserCount, MaxAttLogCount, MaxFingerCount, MaxFaceCount.

Important protocol notes:

  • The ADMS datasheet documents user commands as USER ADD / USER DEL, but real devices reject these with error -1002. Use DATA UPDATE USERINFO / DATA DELETE USERINFO instead.
  • DATA DEL USERINFO (truncated) also fails — the full word DELETE is required.
  • DATA QUERY commands cause the device to push data via POST /iclock/cdata, not via the command confirmation endpoint. The library fully parses USERINFO records pushed this way and dispatches them via the WithOnQueryUsers callback. Use SendQueryUsersCommand to trigger a full user dump, or QueueCommand(sn, "DATA QUERY USERINFO PIN=1") for a single user.
  • Shell commands execute on the device's Linux OS — use with extreme caution.

Registry Payload Parsing

Some ZKTeco devices POST a registry body containing comma-separated key=value pairs, e.g.:

DeviceType=acc,~DeviceName=SpeedFace-V5L-RFID[TI],FirmVer=ZAM180...,IPAddress=192.168.1.201

Notes:

  • Keys can be prefixed with ~. The tilde is stripped when parsed.
  • Values are stored into Device.Options for subsequent inspection.
  • The handler merges all parsed keys into the registered device.

Heartbeat and Online Status

The server updates Device.LastActivity at each request from the device and marks the device online.

  • A device is considered online if its last activity is within the online threshold (default: 2 minutes, configurable via WithOnlineThreshold).
  • The /iclock/inspect endpoint reports for each device:
    • serial: device serial number
    • lastActivity: RFC3339 timestamp of last activity
    • online: boolean derived from last activity
    • options: the registry/options map
    • timezone: effective timezone for timestamp parsing (device → server default → UTC)

Attendance Record Format

Devices send attendance data as tab-separated values:

UserID\tTimestamp\tStatus\tVerifyMode\tWorkCode

Example:

123	2024-01-01 08:00:00	0	1	0

Supported timestamp formats:

  • 2006-01-02 15:04:05 (standard datetime)
  • Unix timestamp (seconds since epoch)

Status Codes

  • 0 - Check In
  • 1 - Check Out
  • 2 - Break Out
  • 3 - Break In
  • 4 - Overtime In
  • 5 - Overtime Out

Verify Mode

These are the ADMS protocol verify mode values observed from real devices. Use zkadms.VerifyModeName(mode) to resolve any value to a human-readable label.

Value Method
0 Password
1 Fingerprint
2 Card (legacy)
3 Password (alternative)
4 Card
5 Fingerprint+Card
6 Fingerprint+Password
7 Card+Password
8 Card+Fingerprint+Password
9 Other
15 Face
25 Palm

Note: Values may vary across device models and firmware versions. The constants VerifyModePassword, VerifyModeFingerprint, VerifyModeCard, VerifyModeFace, and VerifyModePalm are provided for the most common codes.

Examples

See the examples directory for complete examples:

  • basic - Simple server with status endpoint
  • commands - Device command management via REST API
  • database - Integration with database storage

Device Probe Tool

The cmd/probe tool queues candidate commands to a real device and reports which ones succeed vs fail. Useful for discovering what your specific device model supports:

go run ./cmd/probe -addr :8080 -sn YOUR_SERIAL_NUMBER

Use -destructive to include potentially dangerous commands (CONTROL DEVICE, SET OPTION, etc.).

Running Examples

go run ./examples/basic

Then configure your ZKTeco device to connect to:

http://your-server:8080/iclock/

Commands Example

The commands example exposes a REST API for managing devices:

go run ./examples/commands -devices ABCD12345678

Endpoints:

Method Path Description
GET /api/devices List all connected devices
GET /api/devices/{sn} Device detail
POST /api/devices/{sn}/reboot Reboot device
POST /api/devices/{sn}/info Request device info
POST /api/devices/{sn}/check Heartbeat / connectivity check
POST /api/devices/{sn}/sync-time Sync device clock
POST /api/devices/{sn}/clear-data Clear attendance data
POST /api/devices/{sn}/clear-log Clear operation log
POST /api/devices/{sn}/users Add/update user (JSON body)
POST /api/devices/{sn}/users/delete Delete user (JSON body)
POST /api/devices/{sn}/users/query Query all users from device
POST /api/devices/{sn}/open-door Trigger door relay
POST /api/devices/{sn}/get-option Get device option (JSON body)
POST /api/devices/{sn}/shell Execute shell command (JSON body)
POST /api/devices/{sn}/log Request log data
POST /api/devices/{sn}/command Send raw command (JSON body)

Example curl usage:

# Request device info
curl -X POST http://localhost:8080/api/devices/<SN>/info

# Heartbeat check
curl -X POST http://localhost:8080/api/devices/<SN>/check

# Add a user
curl -X POST http://localhost:8080/api/devices/<SN>/users \
     -H 'Content-Type: application/json' \
     -d '{"pin":"1001","name":"John Doe","privilege":0,"card":"12345678"}'

# Delete a user
curl -X POST http://localhost:8080/api/devices/<SN>/users/delete \
     -H 'Content-Type: application/json' \
     -d '{"pin":"1001"}'

# Query all users from device (data pushed via /iclock/cdata)
curl -X POST http://localhost:8080/api/devices/<SN>/users/query

# Get a device option
curl -X POST http://localhost:8080/api/devices/<SN>/get-option \
     -H 'Content-Type: application/json' \
     -d '{"key":"DeviceName"}'

# Execute a shell command on the device (use with caution!)
curl -X POST http://localhost:8080/api/devices/<SN>/shell \
     -H 'Content-Type: application/json' \
     -d '{"command":"date"}'

# Request log data
curl -X POST http://localhost:8080/api/devices/<SN>/log

# Reboot device
curl -X POST http://localhost:8080/api/devices/<SN>/reboot

# Send a raw command
curl -X POST http://localhost:8080/api/devices/<SN>/command \
     -H 'Content-Type: application/json' \
     -d '{"command":"GET OPTION FROM FWVersion"}'

Testing

Run the test suite:

go test -v

Run with race detection and coverage:

go test -v -race -cover

Run benchmarks:

go test -bench=.

API Reference

Types

Option

A function that configures an ADMSServer. Obtained via WithX functions.

ADMSServer

Main server structure handling all protocol operations. Implements http.Handler.

Device

Represents a registered ZKTeco device with SerialNumber, LastActivity, Options, and Timezone fields.

AttendanceRecord

Represents a single attendance transaction with UserID, Timestamp, Status, VerifyMode, WorkCode, and SerialNumber fields.

DeviceSnapshot

JSON representation of a device in the /iclock/inspect response.

CommandResult

Represents the result of a command execution reported by a device. Fields: SerialNumber, ID (int64), ReturnCode (int, 0 = success), Command (string), and QueuedCommand (original queued command string for correlation).

CommandEntry

Pairs a pre-assigned command ID with the command string. Returned by DrainCommands.

UserRecord

Represents a user record returned by a device in response to a DATA QUERY USERINFO command. Fields: PIN, Name, Privilege (int), Card, Password.

DeviceOption

A function that configures a Device during registration. Obtained via WithDevice* functions (e.g. WithDeviceTimezone).

Constructor

NewADMSServer(opts ...Option) *ADMSServer

Creates a new ADMS server instance configured with the given options.

Option Functions

Function Description
WithLogger(*slog.Logger) Set structured logger
WithMaxBodySize(int64) Set max request body size
WithCallbackBufferSize(int) Set callback channel capacity
WithOnlineThreshold(time.Duration) Set device online threshold
WithDispatchTimeout(time.Duration) Set callback dispatch timeout
WithBaseContext(context.Context) Set parent context
WithMaxDevices(int) Set max registered devices (default 1000)
WithUnlimitedDevices() Remove the device registration limit
WithMaxCommandsPerDevice(int) Set max command queue depth per device
WithDeviceEvictionInterval(time.Duration) Set stale-device check interval
WithDeviceEvictionTimeout(time.Duration) Set inactivity threshold for eviction
WithEnableInspect() Enable /iclock/inspect in ServeHTTP router
WithDefaultTimezone(*time.Location) Set fallback timezone for attendance timestamp parsing (default time.UTC)
WithOnAttendance(func(context.Context, AttendanceRecord)) Set attendance callback
WithOnDeviceInfo(func(context.Context, string, map[string]string)) Set device info callback
WithOnRegistry(func(context.Context, string, map[string]string)) Set registry callback
WithOnCommandResult(func(context.Context, CommandResult)) Set command confirmation callback
WithOnQueryUsers(func(context.Context, string, []UserRecord)) Set user query callback

Methods

Method Description
Close() Drain callbacks and stop the worker goroutine
RegisterDevice(serialNumber string, opts ...DeviceOption) error Register a device; validates SN, respects device limit
GetDevice(serialNumber string) *Device Get device information (returns a copy)
SetDeviceTimezone(serialNumber string, loc *time.Location) error Set per-device timezone; returns ErrDeviceNotFound if unknown
GetDeviceTimezone(serialNumber string) *time.Location Get effective timezone (device -> server default -> UTC); nil if unknown
IsDeviceOnline(serialNumber string) bool Check if a device is online
QueueCommand(serialNumber, command string) (int64, error) Queue a command; validates device, fields, and per-device limit
DrainCommands(serialNumber string) []CommandEntry Drain and return all pending commands
PendingCommandsCount(serialNumber string) int Return the number of queued commands without draining
SendInfoCommand(serialNumber string) (int64, error) Queue an INFO command
SendCheckCommand(serialNumber string) (int64, error) Queue a CHECK (heartbeat) command
SendUserAddCommand(serialNumber, pin, name string, privilege int, card string) (int64, error) Queue a DATA UPDATE USERINFO command
SendUserDeleteCommand(serialNumber, pin string) (int64, error) Queue a DATA DELETE USERINFO command
SendGetOptionCommand(serialNumber, key string) (int64, error) Queue a GET OPTION FROM command
SendQueryUsersCommand(serialNumber string) (int64, error) Queue a DATA QUERY USERINFO command
SendShellCommand(serialNumber, command string) (int64, error) Queue a Shell command (use with caution)
SendLogCommand(serialNumber string) (int64, error) Queue a LOG command
ListDevices() []*Device List all registered devices (returns copies)
ServeHTTP(w, r) http.Handler implementation — routes to endpoint handlers
HandleCData(w, r) Handle /iclock/cdata requests
HandleRegistry(w, r) Handle /iclock/registry requests
HandleGetRequest(w, r) Handle /iclock/getrequest requests
HandleDeviceCmd(w, r) Handle /iclock/devicecmd requests
HandleInspect(w, r) Handle /iclock/inspect requests (JSON device snapshot)

Standalone Functions

ParseQueryParams(urlStr string) (map[string]string, error)

Parses URL query parameters into a map.

Sentinel Errors

Error Description
ErrServerClosed Returned when an operation is attempted on a closed server
ErrCallbackQueueFull Returned when the callback queue is full and dispatch timed out
ErrMaxDevicesReached Returned by RegisterDevice when the device limit is reached
ErrCommandQueueFull Returned by QueueCommand when the per-device command limit is reached
ErrInvalidSerialNumber Returned when a serial number is empty, too long, or contains invalid characters
ErrDeviceNotFound Returned when an operation targets a device not registered with the server
ErrInvalidCommandField Returned when a command field contains control characters that could cause injection

Device Configuration

Configure your ZKTeco device to connect to your server:

  1. Access device web interface or admin panel
  2. Chose ADMS
  3. Set server address: http://your-server:8080

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

Support

For issues and questions, please open an issue on GitHub.

About

Golang Library for communicating with ZKTeco biometric devices (ZKTeco ADMS Protocol). Easily integrate, manage, and interact with ZKTeco devices using Go.

Topics

Resources

Contributing

Stars

17 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages