UNPKG

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
'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:',