zklib-ts
Version:
Unofficial zkteco library allows Node.js developers to easily interface with ZK BioMetric Fingerprint Attendance Devices
1,268 lines (1,255 loc) • 128 kB
JavaScript
'use strict';
var net = require('net');
var fs = require('fs');
var dgram = require('node:dgram');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var dgram__namespace = /*#__PURE__*/_interopNamespaceDefault(dgram);
var COMMANDS;
(function (COMMANDS) {
COMMANDS[COMMANDS["CMD_ACK_DATA"] = 2002] = "CMD_ACK_DATA";
/** There was an error when processing the request.*/
COMMANDS[COMMANDS["CMD_ACK_ERROR"] = 2001] = "CMD_ACK_ERROR";
COMMANDS[COMMANDS["CMD_ACK_ERROR_CMD"] = 65533] = "CMD_ACK_ERROR_CMD";
COMMANDS[COMMANDS["CMD_ACK_ERROR_DATA"] = 65531] = "CMD_ACK_ERROR_DATA";
COMMANDS[COMMANDS["CMD_ACK_ERROR_INIT"] = 65532] = "CMD_ACK_ERROR_INIT";
/** [0xD0, 0x07] The request was processed sucessfully. */
COMMANDS[COMMANDS["CMD_ACK_OK"] = 2000] = "CMD_ACK_OK";
COMMANDS[COMMANDS["CMD_ACK_REPEAT"] = 2004] = "CMD_ACK_REPEAT";
COMMANDS[COMMANDS["CMD_ACK_RETRY"] = 2003] = "CMD_ACK_RETRY";
/** [0xD5, 0x07] Connection not authorized. */
COMMANDS[COMMANDS["CMD_ACK_UNAUTH"] = 2005] = "CMD_ACK_UNAUTH";
/** Received unknown command. */
COMMANDS[COMMANDS["CMD_ACK_UNKNOWN"] = 65535] = "CMD_ACK_UNKNOWN";
/** Request attendance log. */
COMMANDS[COMMANDS["CMD_ATTLOG_RRQ"] = 13] = "CMD_ATTLOG_RRQ";
/** [0x4E, 0x04] Request to begin session using commkey. */
COMMANDS[COMMANDS["CMD_AUTH"] = 1102] = "CMD_AUTH";
/** Disable normal authentication of users. */
COMMANDS[COMMANDS["CMD_CANCELCAPTURE"] = 62] = "CMD_CANCELCAPTURE";
/** Capture fingerprint picture. */
COMMANDS[COMMANDS["CMD_CAPTUREFINGER"] = 1009] = "CMD_CAPTUREFINGER";
/** Capture the entire image. */
COMMANDS[COMMANDS["CMD_CAPTUREIMAGE"] = 1012] = "CMD_CAPTUREIMAGE";
/** Change transmission speed. */
COMMANDS[COMMANDS["CMD_CHANGE_SPEED"] = 1101] = "CMD_CHANGE_SPEED";
/** [0x77, 0x00] Get checksum of machine's buffer. */
COMMANDS[COMMANDS["CMD_CHECKSUM_BUFFER"] = 119] = "CMD_CHECKSUM_BUFFER";
/** Restore access control to default. */
COMMANDS[COMMANDS["CMD_CLEAR_ACC"] = 32] = "CMD_CLEAR_ACC";
/** Clears admins privileges. */
COMMANDS[COMMANDS["CMD_CLEAR_ADMIN"] = 20] = "CMD_CLEAR_ADMIN";
/** Delete attendance record. */
COMMANDS[COMMANDS["CMD_CLEAR_ATTLOG"] = 15] = "CMD_CLEAR_ATTLOG";
/** Delete data. */
COMMANDS[COMMANDS["CMD_CLEAR_DATA"] = 14] = "CMD_CLEAR_DATA";
/** Clear screen captions. */
COMMANDS[COMMANDS["CMD_CLEAR_LCD"] = 67] = "CMD_CLEAR_LCD";
/** Delete operations log. */
COMMANDS[COMMANDS["CMD_CLEAR_OPLOG"] = 33] = "CMD_CLEAR_OPLOG";
/** [0xE8, 0x03] Begin connection. */
COMMANDS[COMMANDS["CMD_CONNECT"] = 1000] = "CMD_CONNECT";
/** [0xDD, 0x05] Data packet. */
COMMANDS[COMMANDS["CMD_DATA"] = 1501] = "CMD_DATA";
/** Indicates that it is ready to receive data. */
COMMANDS[COMMANDS["CMD_DATA_RDY"] = 1504] = "CMD_DATA_RDY";
/** Read/Write a large data set. */
COMMANDS[COMMANDS["CMD_DATA_WRRQ"] = 1503] = "CMD_DATA_WRRQ";
/** Read saved data. */
COMMANDS[COMMANDS["CMD_DB_RRQ"] = 7] = "CMD_DB_RRQ";
/** Deletes fingerprint template. */
COMMANDS[COMMANDS["CMD_DEL_FPTMP"] = 134] = "CMD_DEL_FPTMP";
/** Delete short message. */
COMMANDS[COMMANDS["CMD_DELETE_SMS"] = 72] = "CMD_DELETE_SMS";
/** Delete user short message. */
COMMANDS[COMMANDS["CMD_DELETE_UDATA"] = 74] = "CMD_DELETE_UDATA";
/** Delete user. */
COMMANDS[COMMANDS["CMD_DELETE_USER"] = 18] = "CMD_DELETE_USER";
/** Delete user fingerprint template. */
COMMANDS[COMMANDS["CMD_DELETE_USERTEMP"] = 19] = "CMD_DELETE_USERTEMP";
/** Disables fingerprint, rfid reader and keyboard. */
COMMANDS[COMMANDS["CMD_DISABLEDEVICE"] = 1003] = "CMD_DISABLEDEVICE";
/** Get door state. */
COMMANDS[COMMANDS["CMD_DOORSTATE_RRQ"] = 75] = "CMD_DOORSTATE_RRQ";
/** Clear Mifare card. */
COMMANDS[COMMANDS["CMD_EMPTY_MIFARE"] = 78] = "CMD_EMPTY_MIFARE";
/** Enables the ":" in screen clock. */
COMMANDS[COMMANDS["CMD_ENABLE_CLOCK"] = 57] = "CMD_ENABLE_CLOCK";
/** Change machine state to "normal work". */
COMMANDS[COMMANDS["CMD_ENABLEDEVICE"] = 1002] = "CMD_ENABLEDEVICE";
/** [0xE9, 0x03] Disconnect. */
COMMANDS[COMMANDS["CMD_EXIT"] = 1001] = "CMD_EXIT";
/** [0xDE, 0x05] Release buffer used for data transmission. */
COMMANDS[COMMANDS["CMD_FREE_DATA"] = 1502] = "CMD_FREE_DATA";
/** Request machine status (remaining space). */
COMMANDS[COMMANDS["CMD_GET_FREE_SIZES"] = 50] = "CMD_GET_FREE_SIZES";
/** Request max size for users id. */
COMMANDS[COMMANDS["CMD_GET_PINWIDTH"] = 69] = "CMD_GET_PINWIDTH";
/** Request machine time. */
COMMANDS[COMMANDS["CMD_GET_TIME"] = 201] = "CMD_GET_TIME";
COMMANDS[COMMANDS["CMD_GET_USERTEMP"] = 88] = "CMD_GET_USERTEMP";
/** Request the firmware edition. */
COMMANDS[COMMANDS["CMD_GET_VERSION"] = 1100] = "CMD_GET_VERSION";
/** Get group timezone. */
COMMANDS[COMMANDS["CMD_GRPTZ_RRQ"] = 25] = "CMD_GRPTZ_RRQ";
/** Set group timezone. */
COMMANDS[COMMANDS["CMD_GRPTZ_WRQ"] = 26] = "CMD_GRPTZ_WRQ";
/** Read operations log. */
COMMANDS[COMMANDS["CMD_OPLOG_RRQ"] = 34] = "CMD_OPLOG_RRQ";
/** Read configuration value of the machine. */
COMMANDS[COMMANDS["CMD_OPTIONS_RRQ"] = 11] = "CMD_OPTIONS_RRQ";
/** Change configuration value of the machine. */
COMMANDS[COMMANDS["CMD_OPTIONS_WRQ"] = 12] = "CMD_OPTIONS_WRQ";
/** Shut-down machine. */
COMMANDS[COMMANDS["CMD_POWEROFF"] = 1005] = "CMD_POWEROFF";
/** [0xDC, 0x05] Prepare for data transmission. */
COMMANDS[COMMANDS["CMD_PREPARE_DATA"] = 1500] = "CMD_PREPARE_DATA";
/** [0xF5, 0x03] Refresh the machine stored data. */
COMMANDS[COMMANDS["CMD_REFRESHDATA"] = 1013] = "CMD_REFRESHDATA";
/** Refresh the configuration parameters. */
COMMANDS[COMMANDS["CMD_REFRESHOPTION"] = 1014] = "CMD_REFRESHOPTION";
/** Realtime events. */
COMMANDS[COMMANDS["CMD_REG_EVENT"] = 500] = "CMD_REG_EVENT";
/** Restart machine. */
COMMANDS[COMMANDS["CMD_RESTART"] = 1004] = "CMD_RESTART";
/** Change machine state to "awaken". */
COMMANDS[COMMANDS["CMD_RESUME"] = 1007] = "CMD_RESUME";
/** Set machine time. */
COMMANDS[COMMANDS["CMD_SET_TIME"] = 202] = "CMD_SET_TIME";
/** Change machine state to "idle". */
COMMANDS[COMMANDS["CMD_SLEEP"] = 1006] = "CMD_SLEEP";
/** Download short message. */
COMMANDS[COMMANDS["CMD_SMS_RRQ"] = 71] = "CMD_SMS_RRQ";
/** Upload short message. */
COMMANDS[COMMANDS["CMD_SMS_WRQ"] = 70] = "CMD_SMS_WRQ";
/** Start enroll procedure. */
COMMANDS[COMMANDS["CMD_STARTENROLL"] = 61] = "CMD_STARTENROLL";
/** Set the machine to authentication state. */
COMMANDS[COMMANDS["CMD_STARTVERIFY"] = 60] = "CMD_STARTVERIFY";
/** Query state. */
COMMANDS[COMMANDS["CMD_STATE_RRQ"] = 64] = "CMD_STATE_RRQ";
/** Test if fingerprint exists. */
COMMANDS[COMMANDS["CMD_TEST_TEMP"] = 1011] = "CMD_TEST_TEMP";
/** Test voice. */
COMMANDS[COMMANDS["CMD_TESTVOICE"] = 1017] = "CMD_TESTVOICE";
/** [0x77, 0x00] Transfer fp template from buffer. */
COMMANDS[COMMANDS["CMD_TMP_WRITE"] = 87] = "CMD_TMP_WRITE";
/** Get device timezones. */
COMMANDS[COMMANDS["CMD_TZ_RRQ"] = 27] = "CMD_TZ_RRQ";
/** Set device timezones. */
COMMANDS[COMMANDS["CMD_TZ_WRQ"] = 28] = "CMD_TZ_WRQ";
/** Set user short message. */
COMMANDS[COMMANDS["CMD_UDATA_WRQ"] = 73] = "CMD_UDATA_WRQ";
/** Get group combination to unlock. */
COMMANDS[COMMANDS["CMD_ULG_RRQ"] = 29] = "CMD_ULG_RRQ";
/** Set group combination to unlock. */
COMMANDS[COMMANDS["CMD_ULG_WRQ"] = 30] = "CMD_ULG_WRQ";
/** Unlock door for a specified amount of time. */
COMMANDS[COMMANDS["CMD_UNLOCK"] = 31] = "CMD_UNLOCK";
/** Upload user data. */
COMMANDS[COMMANDS["CMD_USER_WRQ"] = 8] = "CMD_USER_WRQ";
/** Read user group. */
COMMANDS[COMMANDS["CMD_USERGRP_RRQ"] = 21] = "CMD_USERGRP_RRQ";
/** Set user group. */
COMMANDS[COMMANDS["CMD_USERGRP_WRQ"] = 22] = "CMD_USERGRP_WRQ";
/** [0x09, 0x00] Read user fingerprint template. */
COMMANDS[COMMANDS["CMD_USERTEMP_RRQ"] = 9] = "CMD_USERTEMP_RRQ";
/** Upload user fingerprint template. */
COMMANDS[COMMANDS["CMD_USERTEMP_WRQ"] = 10] = "CMD_USERTEMP_WRQ";
/** Get user timezones. */
COMMANDS[COMMANDS["CMD_USERTZ_RRQ"] = 23] = "CMD_USERTZ_RRQ";
/** Set the user timezones. */
COMMANDS[COMMANDS["CMD_USERTZ_WRQ"] = 24] = "CMD_USERTZ_WRQ";
/** Read verification style of a given user. */
COMMANDS[COMMANDS["CMD_VERIFY_RRQ"] = 80] = "CMD_VERIFY_RRQ";
/** Change verification style of a given user. */
COMMANDS[COMMANDS["CMD_VERIFY_WRQ"] = 79] = "CMD_VERIFY_WRQ";
/** Prints chars to the device screen. */
COMMANDS[COMMANDS["CMD_WRITE_LCD"] = 66] = "CMD_WRITE_LCD";
/** Write data to Mifare card. */
COMMANDS[COMMANDS["CMD_WRITE_MIFARE"] = 76] = "CMD_WRITE_MIFARE";
/** Triggered alarm. */
COMMANDS[COMMANDS["EF_ALARM"] = 512] = "EF_ALARM";
/** Attendance entry. */
COMMANDS[COMMANDS["EF_ATTLOG"] = 1] = "EF_ATTLOG";
/** Pressed keyboard key. */
COMMANDS[COMMANDS["EF_BUTTON"] = 16] = "EF_BUTTON";
/** Upload user data. */
COMMANDS[COMMANDS["EF_ENROLLFINGER"] = 8] = "EF_ENROLLFINGER";
/** Enrolled user. */
COMMANDS[COMMANDS["EF_ENROLLUSER"] = 4] = "EF_ENROLLUSER";
/** Pressed finger. */
COMMANDS[COMMANDS["EF_FINGER"] = 2] = "EF_FINGER";
/** Fingerprint score in enroll procedure. */
COMMANDS[COMMANDS["EF_FPFTR"] = 256] = "EF_FPFTR";
/** Restore access control to default. */
COMMANDS[COMMANDS["EF_UNLOCK"] = 32] = "EF_UNLOCK";
/** Registered user placed finger. */
COMMANDS[COMMANDS["EF_VERIFY"] = 128] = "EF_VERIFY";
})(COMMANDS || (COMMANDS = {}));
var DISCOVERED_CMD;
(function (DISCOVERED_CMD) {
/** Returned when the Finger id not exists in the user uid, when attempting to download single finger template */
DISCOVERED_CMD[DISCOVERED_CMD["FID_NOT_FOUND"] = 4993] = "FID_NOT_FOUND";
})(DISCOVERED_CMD || (DISCOVERED_CMD = {}));
var Constants;
(function (Constants) {
Constants[Constants["USHRT_MAX"] = 65535] = "USHRT_MAX";
Constants[Constants["MAX_CHUNK"] = 65472] = "MAX_CHUNK";
Constants[Constants["MACHINE_PREPARE_DATA_1"] = 20560] = "MACHINE_PREPARE_DATA_1";
Constants[Constants["MACHINE_PREPARE_DATA_2"] = 32130] = "MACHINE_PREPARE_DATA_2";
})(Constants || (Constants = {}));
const REQUEST_DATA = {
START_TAG: Buffer.from([0x50, 0x50, 0x82, 0x7d]),
DISABLE_DEVICE: Buffer.from([0, 0, 0, 0]),
GET_REAL_TIME_EVENT: Buffer.from([0x01, 0x00, 0x00, 0x00]),
GET_ATTENDANCE_LOGS: Buffer.from([0x01, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
GET_USERS: Buffer.from([0x01, 0x09, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
GET_TEMPLATES: Buffer.from([0x01, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
};
/**
*
* @param {number} time
*/
const decode = time => {
const second = time % 60;
time = (time - second) / 60;
const minute = time % 60;
time = (time - minute) / 60;
const hour = time % 24;
time = (time - hour) / 24;
const day = time % 31 + 1;
time = (time - (day - 1)) / 31;
const month = time % 12;
time = (time - month) / 12;
const year = time + 2000;
return new Date(year, month, day, hour, minute, second);
};
/**
*
* @param {Date} date
*/
const encode = date => {
return (((date.getFullYear() % 100) * 12 * 31 + date.getMonth() * 31 + date.getDate() - 1) * (24 * 60 * 60) +
(date.getHours() * 60 + date.getMinutes()) * 60 +
date.getSeconds());
};
var timeParser = { encode, decode };
const parseCurrentTime = () => {
const currentTime = new Date();
return {
year: currentTime.getFullYear(),
month: currentTime.getMonth() + 1,
day: currentTime.getDate(),
hour: currentTime.getHours(),
second: currentTime.getSeconds()
};
};
const log = (text) => {
const currentTime = parseCurrentTime();
const fileName = `${currentTime.day}`.padStart(2, '0') +
`${currentTime.month}`.padStart(2, '0') +
`${currentTime.year}.err.log`;
const logMessage = `\n [${currentTime.hour}:${currentTime.second}] ${text}`;
fs.appendFile(fileName, logMessage, () => { });
};
/**
* Represents a User as is from ZkDevice and contain methods
* */
class User {
uid;
name;
privilege;
password;
group_id;
user_id;
card;
/**
* Creates a new User instance
* @param uid User ID
* @param name User name
* @param privilege Privilege level
* @param password User password (default: "")
* @param group_id Group ID (default: "")
* @param user_id Alternate user ID (default: "")
* @param card Card number (default: 0)
*/
constructor(uid, name, privilege, password = "", group_id = "", user_id = "", card = 0) {
this.uid = uid;
this.name = name;
this.privilege = privilege;
this.password = password;
this.group_id = group_id;
this.user_id = user_id;
this.card = card;
}
ensureEncoding(string) {
try {
return decodeURIComponent(string);
}
catch (e) {
return unescape(string);
}
}
repack29() {
// Pack format: <BHB5s8sIxBhI (total 29 bytes)
const buf = Buffer.alloc(29);
let offset = 0;
buf.writeUInt8(2, offset);
offset += 1;
buf.writeUInt16LE(this.uid, offset);
offset += 2;
buf.writeUInt8(this.privilege, offset);
offset += 1;
const passwordBuf = Buffer.from(this.ensureEncoding(this.password));
passwordBuf.copy(buf, offset, 0, 5);
offset += 5;
const nameBuf = Buffer.from(this.ensureEncoding(this.name));
nameBuf.copy(buf, offset, 0, 8);
offset += 8;
buf.writeUInt32LE(this.card, offset);
offset += 4;
offset += 1; // padding byte
buf.writeUInt8(0, offset);
offset += 1;
buf.writeUInt32LE(parseInt(this.user_id) || 0, offset);
return buf;
}
repack73() {
// Pack format: <BHB8s24sIB7sx24s (total 73 bytes)
const buf = Buffer.alloc(73);
let offset = 0;
buf.writeUInt8(2, offset);
offset += 1;
buf.writeUInt16LE(this.uid, offset);
offset += 2;
buf.writeUInt8(this.privilege, offset);
offset += 1;
const passwordBuf = Buffer.from(this.ensureEncoding(this.password));
passwordBuf.copy(buf, offset, 0, 8);
offset += 8;
const nameBuf = Buffer.from(this.ensureEncoding(this.name));
nameBuf.copy(buf, offset, 0, 24);
offset += 24;
buf.writeUInt32LE(this.card, offset);
offset += 4;
buf.writeUInt8(1, offset);
offset += 1;
const groupBuf = Buffer.from(this.ensureEncoding(String(this.group_id)));
groupBuf.copy(buf, offset, 0, 7);
offset += 8;
const userIdBuf = Buffer.from(this.ensureEncoding(String(this.user_id)));
userIdBuf.copy(buf, offset, 0, 24);
return buf;
}
}
class Attendance {
/** Internal serial number for the user */
sn;
/** User ID/Pin stored as a string */
user_id;
/** Verification type */
type;
/** Time of the attendance event */
record_time;
/** Verify state */
state;
ip;
constructor(sn, user_id, type, record_time, state) {
this.sn = sn;
this.user_id = user_id;
this.type = type || undefined;
this.record_time = record_time;
this.state = state || undefined;
}
}
const parseHexToTime = (hex) => {
const time = {
year: hex.readUIntLE(0, 1),
month: hex.readUIntLE(1, 1),
date: hex.readUIntLE(2, 1),
hour: hex.readUIntLE(3, 1),
minute: hex.readUIntLE(4, 1),
second: hex.readUIntLE(5, 1)
};
return new Date(2000 + time.year, time.month - 1, time.date, time.hour, time.minute, time.second);
};
const createChkSum = (buf) => {
let chksum = 0;
for (let i = 0; i < buf.length; i += 2) {
if (i === buf.length - 1) {
chksum += buf[i];
}
else {
chksum += buf.readUInt16LE(i);
}
chksum %= Constants.USHRT_MAX;
}
chksum = Constants.USHRT_MAX - chksum - 1;
return chksum;
};
const createUDPHeader = (command, sessionId, replyId, data) => {
const dataBuffer = Buffer.from(data);
const buf = Buffer.alloc(8 + dataBuffer.length);
buf.writeUInt16LE(command, 0);
buf.writeUInt16LE(0, 2);
buf.writeUInt16LE(sessionId, 4);
buf.writeUInt16LE(replyId, 6);
dataBuffer.copy(buf, 8);
const chksum2 = createChkSum(buf);
buf.writeUInt16LE(chksum2, 2);
replyId = (replyId + 1) % Constants.USHRT_MAX;
buf.writeUInt16LE(replyId, 6);
return buf;
};
const createTCPHeader = (command, sessionId, replyId, data) => {
const buf = createUDPHeader(command, sessionId, replyId, data);
const prefixBuf = Buffer.from([0x50, 0x50, 0x82, 0x7d, 0x13, 0x00, 0x00, 0x00]);
prefixBuf.writeUInt16LE(buf.length, 4);
return Buffer.concat([prefixBuf, buf]);
};
const removeTcpHeader = (buf) => {
if (buf.length < 8) {
return buf;
}
if (buf.compare(Buffer.from([0x50, 0x50, 0x82, 0x7d]), 0, 4, 0, 4) !== 0) {
return buf;
}
return buf.slice(8);
};
const parseTimeToDate = (time) => {
const second = time % 60;
time = (time - second) / 60;
const minute = time % 60;
time = (time - minute) / 60;
const hour = time % 24;
time = (time - hour) / 24;
const day = time % 31 + 1;
time = (time - (day - 1)) / 31;
const month = time % 12;
time = (time - month) / 12;
const year = time + 2000;
return new Date(Date.UTC(year, month, day, hour, minute, second));
};
const decodeUserData28 = (userData) => {
return {
uid: userData.readUIntLE(0, 2),
privilege: userData.readUIntLE(2, 1),
name: userData
.slice(8, 8 + 8)
.toString('ascii')
.split('\0')
.shift() || '',
user_id: userData.readUIntLE(24, 4).toString(),
};
};
const decodeUserData72 = (userData) => {
return new User(userData.readUIntLE(0, 2), userData
.slice(11)
.toString('ascii')
.split('\0')
.shift() || '', userData.readUIntLE(2, 1), userData
.subarray(3, 3 + 8)
.toString('ascii')
.split('\0')
.shift() || '', userData.readUIntLE(39, 1), userData
.slice(48, 48 + 9)
.toString('ascii')
.split('\0')
.shift() || '', userData.readUIntLE(35, 4));
};
const decodeRecordData40 = (recordData) => {
return new Attendance(recordData.readUIntLE(0, 2), recordData
.slice(2, 2 + 9)
.toString('ascii')
.split('\0')
.shift() || '', recordData.readUIntLE(26, 1), parseTimeToDate(recordData.readUInt32LE(27)), recordData.readUIntLE(31, 1));
};
const decodeRecordData16 = (recordData) => {
return {
user_id: recordData.readUIntLE(0, 2).toString(),
record_time: parseTimeToDate(recordData.readUInt32LE(4))
};
};
const decodeRecordRealTimeLog18 = (recordData) => {
const user_id = recordData.readUIntLE(8, 1).toString();
const record_time = parseHexToTime(recordData.subarray(12, 18));
return { user_id, record_time };
};
const decodeRecordRealTimeLog52 = (recordData) => {
const payload = removeTcpHeader(recordData);
const recvData = payload.subarray(8);
const user_id = recvData.slice(0, 9)
.toString('ascii')
.split('\0')
.shift() || '';
const record_time = parseHexToTime(recvData.subarray(26, 26 + 6));
return { user_id, record_time };
};
const decodeUDPHeader = (header) => {
return {
commandId: header.readUIntLE(0, 2),
checkSum: header.readUIntLE(2, 2),
sessionId: header.readUIntLE(4, 2),
replyId: header.readUIntLE(6, 2)
};
};
const decodeTCPHeader = (header) => {
const recvData = header.subarray(8);
const payloadSize = header.readUIntLE(4, 2);
return {
commandId: recvData.readUIntLE(0, 2),
checkSum: recvData.readUIntLE(2, 2),
sessionId: recvData.readUIntLE(4, 2),
replyId: recvData.readUIntLE(6, 2),
payloadSize
};
};
const exportErrorMessage = (commandValue) => {
const keys = Object.keys(COMMANDS);
for (const key of keys) {
if (COMMANDS[key] === commandValue) {
return key.toString();
}
}
return 'AN UNKNOWN ERROR';
};
const checkNotEventTCP = (data) => {
try {
const cleanedData = removeTcpHeader(data);
const commandId = cleanedData.readUIntLE(0, 2);
const event = cleanedData.readUIntLE(4, 2);
return event === COMMANDS.EF_ATTLOG && commandId === COMMANDS.CMD_REG_EVENT;
}
catch (err) {
log(`[228] : ${err.toString()} ,${data.toString('hex')} `);
return false;
}
};
const checkNotEventUDP = (data) => {
const { commandId } = decodeUDPHeader(data.subarray(0, 8));
return commandId === COMMANDS.CMD_REG_EVENT;
};
const makeKey = (key, sessionId) => {
let k = 0;
for (let i = 0; i < 32; i++) {
if ((key & (1 << i)) !== 0) {
k = (k << 1) | 1;
}
else {
k = k << 1;
}
}
k += sessionId;
let hex = k.toString(16).padStart(8, "0");
let response = new Uint8Array(4);
let index = 3;
while (hex.length > 0) {
response[index] = parseInt(hex.substring(0, 2), 16);
index--;
hex = hex.substring(2);
}
response[0] ^= 'Z'.charCodeAt(0);
response[1] ^= 'K'.charCodeAt(0);
response[2] ^= 'S'.charCodeAt(0);
response[3] ^= 'O'.charCodeAt(0);
let finalKey = response[0] +
(response[1] << 8) +
(response[2] << 16) +
(response[3] << 24);
let swp = finalKey >>> 16;
finalKey = (finalKey << 16) | swp;
return finalKey >>> 0;
};
const authKey = (comKey, sessionId) => {
let k = makeKey(comKey, sessionId) >>> 0;
let rand = Math.floor(Math.random() * 256);
let hex = k.toString(16).padStart(8, "0");
let response = new Uint8Array(4);
let index = 3;
while (index >= 0) {
response[index] = parseInt(hex.substring(0, 2), 16);
index--;
hex = hex.substring(2);
}
response[0] ^= rand;
response[1] ^= rand;
response[2] = rand;
response[3] ^= rand;
return Array.from(response);
};
/**
* Error types for device communication
*/
const ERROR_TYPES = {
ECONNRESET: 'ECONNRESET',
ECONNREFUSED: 'ECONNREFUSED'};
/**
* Custom error class for device communication errors
*/
class ZkError {
err;
ip;
command;
/**
* Creates a new ZkError instance
* @param err The error object
* @param command The command that caused the error
* @param ip The IP address of the device
*/
constructor(err, command, ip) {
this.err = err;
this.ip = ip;
this.command = command;
}
/**
* Gets a user-friendly error message
* @returns A formatted error message
*/
toast() {
if (this.err.code === ERROR_TYPES.ECONNRESET) {
return 'Another device is connecting to the device so the connection is interrupted';
}
else if (this.err.code === ERROR_TYPES.ECONNREFUSED) {
return 'IP of the device is refused';
}
return this.err.message;
}
/**
* Gets detailed error information
* @returns An object containing error details
*/
getError() {
return {
err: {
message: this.err.message,
code: this.err.code
},
ip: this.ip,
command: this.command
};
}
}
/**
* Represents a fingerprint template with associated metadata
*/
class Finger {
uid;
fid;
valid;
template;
size;
mark;
/**
* Creates a new Finger instance
* @param uid User internal reference
* @param fid Finger ID (value >= 0 && value <= 9)
* @param valid Flag indicating 0 = invalid | 1 = valid | 3 = duress
* @param template Fingerprint template data buffer
*/
constructor(uid, fid, valid, template) {
this.uid = Number(uid);
this.fid = Number(fid);
this.valid = Number(valid);
this.template = template;
this.size = template.length;
// Create mark showing first and last 8 bytes as hex
const start = Uint8Array.prototype.slice.call(template, 0, 8).toString('hex');
const end = Uint8Array.prototype.slice.call(template, -8).toString('hex');
this.mark = `${start}...${end}`;
}
/**
* Packs the fingerprint data with metadata into a Buffer
* @returns Buffer containing packed fingerprint data
*/
repack() {
// pack("HHbb%is" % (self.size), self.size+6, self.uid, self.fid, self.valid, self.template)
const buf = Buffer.alloc(6 + this.size); // HHbb = 6 bytes + template size
let offset = 0;
buf.writeUInt16LE(this.size + 6, offset);
offset += 2;
buf.writeUInt16LE(this.uid, offset);
offset += 2;
buf.writeUInt8(this.fid, offset);
offset += 1;
buf.writeUInt8(this.valid, offset);
offset += 1;
this.template.copy(buf, offset);
return buf;
}
/**
* Packs only the fingerprint template data into a Buffer
* @returns Buffer containing just the template data
*/
repackOnly() {
// pack("H%is" % (self.size), self.size, self.template)
const buf = Buffer.alloc(2 + this.size); // H = 2 bytes + template size
buf.writeUInt16LE(this.size, 0);
this.template.copy(buf, 2);
return buf;
}
/**
* Compares this fingerprint with another for equality
* @param other Another Finger instance to compare with
* @returns true if all properties and template data match
*/
equals(other) {
if (!(other instanceof Finger))
return false;
return this.uid === other.uid &&
this.fid === other.fid &&
this.valid === other.valid &&
this.template.equals(other.template);
}
}
class UserService {
_zkTcp;
_users;
constructor(zkTcp) {
this._zkTcp = zkTcp;
}
async getUserByUserId(user_id) {
if (!this._users) {
await this.getUsers();
}
if (this._users.has(String(user_id))) {
return this._users.get(String(user_id));
}
else
throw new Error("user_id not exists");
}
async getUsers() {
try {
// Free any existing buffer data to prepare for a new request
if (this._users) {
return { data: Array.from(this._users.values()) };
}
else {
this._users = new Map([]);
}
if (this._zkTcp.socket) {
await this._zkTcp.freeData();
}
// Request user data
const data = await this._zkTcp.readWithBuffer(REQUEST_DATA.GET_USERS);
// Free buffer data after receiving the data
if (this._zkTcp.socket) {
await this._zkTcp.freeData();
}
// Constants for user data processing
const USER_PACKET_SIZE = 72;
// Ensure data.data is a valid buffer
if (!data.data || !(data.data instanceof Buffer)) {
throw new Error('Invalid data received');
}
let userData = data.data.subarray(4); // Skip the first 4 bytes (headers)
const users = [];
// Process each user packet
while (userData.length >= USER_PACKET_SIZE) {
// Decode user data and add to the users array
const user = decodeUserData72(userData.subarray(0, USER_PACKET_SIZE));
users.push(user);
this._users.set(user.user_id, user);
userData = userData.subarray(USER_PACKET_SIZE); // Move to the next packet
}
// Return the list of users
return { data: users };
}
catch (err) {
// Log the error for debugging
console.error('Error getting users:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async setUser(user_id, name, password, role = 0, cardno = 0) {
let user;
try {
user = await this.getUserByUserId(user_id);
}
catch (err) {
if (err.message.includes("user_id not exists")) {
user.uid = Math.max(...Array.from(this._users.values()).map(usr => usr.uid)) + 1;
this._users.set(user_id, user);
}
}
try {
// Validate input parameters
if (user_id.length > 9 ||
name.length > 24 ||
password.length > 8 ||
typeof role !== 'number' ||
cardno.toString().length > 10) {
throw new Error('Invalid input parameters');
}
// Allocate and initialize the buffer
const commandBuffer = Buffer.alloc(72);
// Fill the buffer with user data
commandBuffer.writeUInt16LE(user.uid, 0);
commandBuffer.writeUInt16LE(role, 2);
commandBuffer.write(password.padEnd(8, '\0'), 3, 8); // Ensure password is 8 bytes
commandBuffer.write(name.padEnd(24, '\0'), 11, 24); // Ensure name is 24 bytes
commandBuffer.writeUInt16LE(cardno, 35);
commandBuffer.writeUInt32LE(0, 40); // Placeholder or reserved field
commandBuffer.write(user_id.padEnd(9, '\0'), 48, 9); // Ensure userid is 9 bytes
// Send the command and return the result
const created = await this._zkTcp.executeCmd(COMMANDS.CMD_USER_WRQ, commandBuffer);
return !!created;
}
catch (err) {
// Log error details for debugging
console.error('Error setting user:', err);
// Re-throw error for upstream handling
throw err;
}
}
async DeleteUser(user_id) {
try {
const user = await this.getUserByUserId(user_id);
// Allocate and initialize the buffer
const commandBuffer = Buffer.alloc(72);
// Write UID to the buffer
commandBuffer.writeUInt16LE(user.uid, 0);
// Send the delete command and return the result
const deleted = await this._zkTcp.executeCmd(COMMANDS.CMD_DELETE_USER, commandBuffer);
return !!deleted;
}
catch (err) {
// Log error details for debugging
console.error('Error deleting user:', err);
// Re-throw error for upstream handling
throw err;
}
}
async getTemplates(callbackInProcess = () => { }) {
let templates = [];
try {
if (this._zkTcp.socket) {
await this._zkTcp.freeData();
}
await this._zkTcp.getSizes();
if (this._zkTcp.fp_count == 0)
return { data: [] };
await this._zkTcp.disableDevice();
const resp = await this._zkTcp.readWithBuffer(REQUEST_DATA.GET_TEMPLATES);
let templateData = resp.data.subarray(4);
let totalSize = resp.data.readUIntLE(0, 4);
while (totalSize) {
const buf = templateData.subarray(0, 6);
const size = buf.readUIntLE(0, 2);
const uid = buf.readUIntLE(2, 2);
const fid = buf.readUIntLE(4, 1);
const valid = buf.readUIntLE(5, 1);
// Force-copy bytes so we don't retain the entire big backing buffer
const tplBytes = Buffer.from(templateData.subarray(6, size));
templates.push(new Finger(uid, fid, valid, tplBytes));
templateData = templateData.subarray(size);
totalSize -= size;
}
return { data: templates };
}
catch (err) {
this._zkTcp.verbose && console.log("Error getting templates", err);
return { data: templates };
}
finally {
await this._zkTcp.freeData();
await this._zkTcp.enableDevice();
}
}
async DownloadFp(user_id, fid) {
try {
const user = await this.getUserByUserId(user_id);
if (0 > fid || fid > 9)
throw new Error('fid must be between 0 and 9');
// Allocate and initialize the buffer
const data = Buffer.alloc(3);
// Fill the buffer with user data
data.writeUInt16LE(user.uid, 0);
data.writeUIntLE(fid, 2, 1);
this._zkTcp.replyId++;
const packet = createTCPHeader(COMMANDS.CMD_USERTEMP_RRQ, this._zkTcp.sessionId, this._zkTcp.replyId, data);
let fingerSize = 0;
let fingerTemplate = Buffer.from([]);
return await new Promise((resolve, reject) => {
let timeout;
const cleanup = () => {
if (this._zkTcp.socket) {
this._zkTcp.socket.removeListener('data', receiveData);
}
if (timeout)
clearTimeout(timeout);
};
let timer = () => setTimeout(() => {
cleanup();
reject(new Error('Time Out, Could not retrieve data'));
}, this._zkTcp.timeout);
const receiveData = (data) => {
timeout = timer();
if (data.length === 0)
return;
try {
if (data.length == 0)
return;
const headers = decodeTCPHeader(data);
switch (headers.commandId) {
case DISCOVERED_CMD.FID_NOT_FOUND:
throw new Error('Could not retrieve data. maybe finger id not exists?');
case COMMANDS.CMD_PREPARE_DATA:
fingerSize = data.readUIntLE(16, 2);
break;
case COMMANDS.CMD_DATA:
// A single 'data' event might contain multiple TCP packets combined by the OS
// in this method, is possible to get CMD_DATA and CMD_ACK_OK in the same event,
// so It's important to split data received for remove CMD_ACK_OK headers
fingerTemplate = Buffer.concat([fingerTemplate, data.subarray(16, fingerSize + 10)]);
// @ts-ignore
resolve(fingerTemplate);
break;
case COMMANDS.CMD_ACK_OK:
cleanup();
// @ts-ignore
resolve(fingerTemplate);
return;
default:
// If it's not a recognized command but has data, it might be raw template data
if (headers.commandId > 2000 && headers.commandId < 3000) {
// Likely another ACK or system msg
}
else {
fingerTemplate = Buffer.concat([fingerTemplate, data]);
}
break;
}
clearTimeout(timeout);
}
catch (e) {
cleanup();
reject(e);
}
};
if (this._zkTcp.socket) {
this._zkTcp.socket.on('data', receiveData);
this._zkTcp.socket.write(packet, (err) => {
if (err) {
cleanup();
reject(err);
}
});
}
else {
reject(new Error('Socket not initialized'));
}
});
}
catch (err) {
throw err;
}
finally {
await this._zkTcp.refreshData();
}
}
/**
*
* @param user_id {string} user
* @param fingers {Finger[]} array of finger templates instances
* */
async saveTemplates(user_id, fingers = []) {
if (fingers.length > 9 || fingers.length == 0)
throw new Error("maximum finger length is 10 and can't be empty");
try {
await this._zkTcp.disableDevice();
// check users exists
const user = await this.getUserByUserId(user_id);
let fpack = Buffer.alloc(0);
let table = Buffer.alloc(0);
const fnum = 0x10;
let tstart = 0;
for (const finger of fingers) {
const tfp = finger.repackOnly();
const tableEntry = Buffer.alloc(11); // b=1, H=2, b=1, I=4 => 1+2+1+4=8? Wait, bHbI is 1+2+1+4=8 bytes
tableEntry.writeInt8(2, 0);
tableEntry.writeUInt16LE(user.uid, 1);
tableEntry.writeInt8(fnum + finger.fid, 3);
tableEntry.writeUInt32LE(tstart, 4);
table = Buffer.concat([table, tableEntry]);
tstart += tfp.length;
fpack = Buffer.concat([fpack, tfp]);
}
let upack;
if (this._zkTcp.userPacketSize === 28) {
upack = user.repack29();
}
else {
upack = user.repack73();
}
const head = Buffer.alloc(12); // III = 3*4 bytes
head.writeUInt32LE(upack.length, 0);
head.writeUInt32LE(table.length, 4);
head.writeUInt32LE(fpack.length, 8);
const packet = Buffer.concat([head, upack, table, fpack]);
const bufferResponse = await this._zkTcp.sendWithBuffer(packet);
const command = 110;
const commandString = Buffer.alloc(8); // <IHH = I(4) + H(2) + H(2) = 8 bytes
commandString.writeUInt32LE(12, 0);
commandString.writeUInt16LE(0, 4);
commandString.writeUInt16LE(8, 6);
const cmdResponse = await this._zkTcp.executeCmd(command, commandString);
if (this._zkTcp.verbose)
console.log("finally bulk save user templates: \n", cmdResponse.readUInt16LE(0));
}
catch (error) {
throw error;
}
finally {
await this._zkTcp.refreshData();
await this._zkTcp.enableDevice();
}
}
async deleteFinger(user_id, fid) {
try {
if (!this._users.has(user_id))
throw new Error("user_id not exists");
const user = await this.getUserByUserId(user_id);
const buf = Buffer.alloc(4);
buf.writeUInt16LE(user_id ? user.uid : 0, 0);
buf.writeUint16LE(fid ? fid : 0, 2);
const reply = await this._zkTcp.executeCmd(COMMANDS.CMD_DELETE_USERTEMP, buf);
return !!reply;
}
catch (error) {
throw new Error("Can't save utemp");
}
finally {
await this._zkTcp.refreshData();
}
}
async enrollInfo(user_id, tempId) {
let done = false;
try {
const userBuf = Buffer.alloc(24);
userBuf.write(user_id, 0, 24, 'ascii');
let commandString = Buffer.concat([
userBuf,
Buffer.from([tempId, 1])
]);
const sendAckOk = async () => {
try {
const buf = createTCPHeader(COMMANDS.CMD_ACK_OK, this._zkTcp.sessionId, Constants.USHRT_MAX - 1, Buffer.from([]));
this._zkTcp.socket.write(buf);
}
catch (e) {
throw new ZkError(e, COMMANDS.CMD_ACK_OK, this._zkTcp.ip);
}
};
const cancel = await this._zkTcp.cancelCapture();
const cmdResponse = await this._zkTcp.executeCmd(COMMANDS.CMD_STARTENROLL, commandString);
this._zkTcp.timeout = 60000; // 60 seconds timeout
let attempts = 3;
while (attempts > 0) {
if (this._zkTcp.verbose)
console.log(`A:${attempts} esperando primer regevent`);
let dataRecv = await this._zkTcp.readSocket(17);
await sendAckOk();
if (dataRecv.length > 16) {
const padded = Buffer.concat([dataRecv, Buffer.alloc(24 - dataRecv.length)]);
const res = padded.readUInt16LE(16);
if (this._zkTcp.verbose)
console.log(`res ${res}`);
if (res === 0 || res === 6 || res === 4) {
if (this._zkTcp.verbose)
console.log("posible timeout o reg Fallido");
break;
}
}
if (this._zkTcp.verbose)
console.log(`A:${attempts} esperando 2do regevent`);
dataRecv = await this._zkTcp.readSocket(17);
await sendAckOk();
if (this._zkTcp.verbose)
console.log(dataRecv);
if (dataRecv.length > 8) {
const padded = Buffer.concat([dataRecv, Buffer.alloc(24 - dataRecv.length)]);
const res = padded.readUInt16LE(16);
if (this._zkTcp.verbose)
console.log(`res ${res}`);
if (res === 6 || res === 4) {
if (this._zkTcp.verbose)
console.log("posible timeout o reg Fallido");
break;
}
else if (res === 0x64) {
if (this._zkTcp.verbose)
console.log("ok, continue?");
attempts--;
}
}
}
if (attempts === 0) {
const dataRecv = await this._zkTcp.readSocket(17);
await sendAckOk();
if (this._zkTcp.verbose)
console.log(dataRecv.toString('hex'));
const padded = Buffer.concat([dataRecv, Buffer.alloc(24 - dataRecv.length)]);
let res = padded.readUInt16LE(16);
if (this._zkTcp.verbose)
console.log(`res ${res}`);
if (res === 5) {
if (this._zkTcp.verbose)
console.log("finger duplicate");
}
if (res === 6 || res === 4) {
if (this._zkTcp.verbose)
console.log("posible timeout");
}
if (res === 0) {
const size = padded.readUInt16LE(10);
const pos = padded.readUInt16LE(12);
if (this._zkTcp.verbose)
console.log(`enroll ok ${size} ${pos}`);
done = true;
}
}
//this.__sock.setTimeout(this.__timeout);
await this._zkTcp.regEvent(0); // TODO: test
return done;
}
catch (error) {
throw error;
}
finally {
await this._zkTcp.cancelCapture();
await this.verify(user_id);
}
}
async verify(user_id) {
try {
const user = await this.getUserByUserId(user_id);
const command_string = Buffer.alloc(4);
command_string.writeUInt32LE(user.uid, 0);
const reply = await this._zkTcp.executeCmd(COMMANDS.CMD_STARTVERIFY, command_string);
if (this._zkTcp.verbose)
console.log(reply.readUInt16LE(0));
return !!reply;
}
catch (error) {
console.error(error);
throw error;
}
}
/**
* Upload a single fingerprint for a given user id
* @param user_id {string} user id for customer
* @param fingerTemplate {string} finger template in base64 string
* @param fid {number} finger id is a number between 0 and 9
* @param fp_valid {number} finger flag. e.g., valid=1, duress=3
*/
async uploadFingerTemplate(user_id, fingerTemplate, fid, fp_valid) {
try {
const check_ACK_OK = (buf) => {
let resp_cmd = initPacket.readUInt16LE(0);
if (resp_cmd === COMMANDS.CMD_ACK_OK)
return true;
else
throw new Error(`received unexpected command: ${resp_cmd}`);
};
const user = this._users.get(user_id);
await this._zkTcp.disableDevice();
const prep_struct = Buffer.alloc(4);
const fingerBuffer = Buffer.from(fingerTemplate, 'base64');
const fp_size = fingerBuffer.length;
prep_struct.writeUInt16LE(fp_size, 0);
const initPacket = await this._zkTcp.executeCmd(COMMANDS.CMD_PREPARE_DATA, prep_struct);
check_ACK_OK(initPacket);
const fpPacket = await this._zkTcp.executeCmd(COMMANDS.CMD_DATA, fingerBuffer);
check_ACK_OK(fpPacket);
const cheksumPacket = await this._zkTcp.executeCmd(COMMANDS.CMD_CHECKSUM_BUFFER, '');
check_ACK_OK(cheksumPacket);
const checksum = cheksumPacket.readUInt32LE(8);
const tmp_wreq = Buffer.alloc(6);
tmp_wreq.writeUInt16LE(user.uid, 0);
tmp_wreq.writeUIntLE(fid, 2, 1);
tmp_wreq.writeUIntLE(fp_valid, 3, 1);
tmp_wreq.writeUInt16LE(fp_size, 4);
const tmp_wreqPacket = await this._zkTcp.executeCmd(COMMANDS.CMD_TMP_WRITE, tmp_wreq);
check_ACK_OK(tmp_wreqPacket);
const freeData = await this._zkTcp.executeCmd(COMMANDS.CMD_FREE_DATA, '');
return check_ACK_OK(freeData);
}
catch (err) {
throw err;
}
finally {
await this._zkTcp.refreshData();
await this._zkTcp.enableDevice();
}
}
}
class TransactionService {
_zkTcp;
constructor(zkTcp) {
this._zkTcp = zkTcp;
}
async getAttendances(callbackInProcess = () => { }) {
try {
// Free any existing buffer data to prepare for a new request
if (this._zkTcp.socket) {
await this._zkTcp.freeData();
}
// Request attendance logs and handle chunked data
const data = await this._zkTcp.readWithBuffer(REQUEST_DATA.GET_ATTENDANCE_LOGS, callbackInProcess);
// Free buffer data after receiving the attendance logs
if (this._zkTcp.socket) {
await this._zkTcp.freeData();
}
// Constants for record processing
const RECORD_PACKET_SIZE = 40;
// Ensure data.data is a valid buffer
if (!data.data || !(data.data instanceof Buffer)) {
throw new Error('Invalid data received');
}
// Process the record data
let recordData = data.data.subarray(4); // Skip header
const records = [];
// Process each attendance record
while (recordData.length >= RECORD_PACKET_SIZE) {
const record = decodeRecordData40(recordData.subarray(0, RECORD_PACKET_SIZE));
records.push({ ...record, ip: this._zkTcp.ip }); // Add IP address to each record
recordData = recordData.subarray(RECORD_PACKET_SIZE); // Move to the next packet
}
// Return the list of attendance records
return { data: records };
}
catch (err) {
// Log and re-throw the error
console.error('Error getting attendance records:', err);
throw err; // Re-throw the error for handling by the caller
}
}
// Clears the attendance logs on the device
async clearAttendanceLog() {
try {
// Execute the command to clear attendance logs
await this._zkTcp.disableDevice();
const buf = await this._zkTcp.executeCmd(COMMANDS.CMD_CLEAR_ATTLOG, '');
await this._zkTcp.refreshData();
await this._zkTcp.enableDevice();
return !!buf;
}
catch (err) {
// Log the error for debugging purposes
console.error('Error clearing attendance log:',