UNPKG

zklib-ts

Version:

Unofficial zkteco library allows Node.js developers to easily interface with ZK BioMetric Fingerprint Attendance Devices

1,248 lines (1,239 loc) 146 kB
import { Socket } from 'net'; import { appendFile } from 'fs'; import * as dgram from 'node: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"; /** [0x3e, 0x00] 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"; /** [0x3d, 0x00] Start enroll procedure. */ COMMANDS[COMMANDS["CMD_STARTENROLL"] = 61] = "CMD_STARTENROLL"; /** [0x3c, 0x00] 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"; })(COMMANDS || (COMMANDS = {})); var RTEvent; (function (RTEvent) { /** Triggered alarm. */ RTEvent[RTEvent["EF_ALARM"] = 512] = "EF_ALARM"; /** Attendance entry. */ RTEvent[RTEvent["EF_ATTLOG"] = 1] = "EF_ATTLOG"; /** Pressed keyboard key. */ RTEvent[RTEvent["EF_BUTTON"] = 16] = "EF_BUTTON"; /** Upload user data. */ RTEvent[RTEvent["EF_ENROLLFINGER"] = 8] = "EF_ENROLLFINGER"; /** Enrolled user. */ RTEvent[RTEvent["EF_ENROLLUSER"] = 4] = "EF_ENROLLUSER"; /** Pressed finger. */ RTEvent[RTEvent["EF_FINGER"] = 2] = "EF_FINGER"; /** Fingerprint score in enroll procedure. */ RTEvent[RTEvent["EF_FPFTR"] = 256] = "EF_FPFTR"; /** Restore access control to default. */ RTEvent[RTEvent["EF_UNLOCK"] = 32] = "EF_UNLOCK"; /** Registered user placed finger. */ RTEvent[RTEvent["EF_VERIFY"] = 128] = "EF_VERIFY"; })(RTEvent || (RTEvent = {})); var DISCOVERED_CMD; (function (DISCOVERED_CMD) { /** [0x7f, 0x13] */ DISCOVERED_CMD[DISCOVERED_CMD["UNKNOWN"] = 4991] = "UNKNOWN"; /** 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"; /** [0x87, 0x13] i guess is an error reply code, is returned when attempint to read options ~isABCPinEnable and ~T9FunOn */ DISCOVERED_CMD[DISCOVERED_CMD["UNKNOWN_OR_NOT_SUPPORTED"] = 4999] = "UNKNOWN_OR_NOT_SUPPORTED"; })(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]) }; /** * 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 }; } } 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}`; 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; } } /** * Represents an Attendance Records */ 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 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 isRTEvent = (event) => Object.values(RTEvent).includes(event); const checkNotEventTCP = (data) => { try { const cleanedData = removeTcpHeader(data); const commandId = cleanedData.readUIntLE(0, 2); const event = cleanedData.readUIntLE(4, 2); return isRTEvent(event) && 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 decodeRTEvent = (data) => { const header = decodeTCPHeader(data); console.log(header); const event = header.sessionId; const recvData = data.subarray(16); const build = { alarm4bytes: function (data) { return { alarm_type: data.readUintLE(0, 4), alarm_cause: data.readUintLE(4, 2), user_uid: data.readUintLE(6, 2), match_type: data.readUintLE(8, 4), }; }, enrolledFinger: function (data) { return { result: data.readUIntLE(0, 2), size: data.readUintLE(2, 2), pin: data.subarray(4, 4 + 9).toString("ascii"), fid: data.readUIntLE(1, 13), }; }, fingerScore: function (data) { return { score: data.readUintLE(0, 1) }; }, }; switch (event) { case RTEvent.EF_FINGER: return { event }; case RTEvent.EF_FPFTR: return { event, payload: build.fingerScore(recvData) }; default: console.log("utils linea 284"); break; } }; 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"); const 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); const swp = finalKey >>> 16; finalKey = (finalKey << 16) | swp; return finalKey >>> 0; }; const authKey = (comKey, sessionId) => { const k = makeKey(comKey, sessionId) >>> 0; const rand = Math.floor(Math.random() * 256); let hex = k.toString(16).padStart(8, "0"); const 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); }; /** Some Tcp packets receibed by the socket client can be merged in one single data event by the OS, and we need to split to handle correctly */ const splitTcpPackets = (data) => { const packets = []; let unProcessed = data; while (unProcessed.length > 0) { const headers = decodeTCPHeader(unProcessed); packets.push(unProcessed.subarray(0, headers.payloadSize + 8)); unProcessed = unProcessed.subarray(headers.payloadSize + 8); } return packets; }; /** * Represents a fingerprint template with associated metadata */ class Finger { uid; fid; /** Flag indicating 0 = invalid | 1 = valid | 3 = duress. if is not initilizaed, default is 1 = valid */ 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, template, valid) { this.uid = Number(uid); this.fid = Number(fid); this.valid = valid ? Number(valid) : 1; 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(); } await this._zkTcp.disableDevice(); const users = await new Promise((resolve, reject) => { // Request user data this._zkTcp .readWithBuffer(REQUEST_DATA.GET_USERS) .then(async (data) => { // Ensure data.data is a valid buffer if (!data.data || !(data.data instanceof Buffer)) { reject(new Error("Invalid data received")); return; } let userData = data.data.subarray(4); // Skip the first 4 bytes (headers) const users = []; // Constants for user data processing const USER_PACKET_SIZE = 72; // 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 } resolve(users); }) .catch(reject); }); // Free buffer data after receiving the data if (this._zkTcp.socket && users) { await this._zkTcp.enableDevice(); await this._zkTcp.freeData(); } // 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) { 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(0, 0); // uid will be set in the device 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(cb) { const templates = []; try { await this._zkTcp.disableDevice(); if (this._zkTcp.socket) { await this._zkTcp.freeData(); } if (!this._zkTcp.fp_count) { await this._zkTcp.getSizes(); } if (this._zkTcp.fp_count == 0) return { data: [] }; 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, tplBytes, valid)); templateData = templateData.subarray(size); totalSize -= size; } if (cb) cb(templates); return { data: templates }; } catch (err) { this._zkTcp.verbose && console.log("Error getting templates", err); throw err; } finally { await this._zkTcp.freeData(); await this._zkTcp.enableDevice(); } } async DownloadFp(user_id, fid) { try { const user = (await this.getUserByUserId(String(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([]); const template = 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, undefined, (err) => { if (err) { cleanup(); reject(err); } }); } else { reject(new Error("Socket not initialized")); } }); return new Finger(user.uid, fid, template); } catch (err) { throw err; } finally { await this._zkTcp.freeData(); } } /** * * @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]); 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); await this._zkTcp.executeCmd(command, commandString); if (this._zkTcp.verbose) console.log("finally bulk save user templates: \n", "templates saved successfully"); } catch (error) { throw error; } finally { await this._zkTcp.refreshData(); await this._zkTcp.enableDevice(); } } async deleteFinger(user_id, fid) { try { 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) { try { let timer; const setTimeoutTimer = (cb) => { if (timer) clearTimeout(timer); return setTimeout(() => { cb(new Error("[ENROLL_INFO] time out")); }, 1000 * 20); }; const cleanUp = () => { if (timer) clearTimeout(timer); }; return await new Promise((resolve, reject) => { const handleRtEvent = (rtEvent) => { switch (rtEvent.event) { case RTEvent.EF_FPFTR: timer = setTimeoutTimer(reject); break; case RTEvent.EF_FINGER: console.log(rtEvent); break; case RTEvent.EF_ENROLLFINGER: cleanUp(); resolve(rtEvent); break; default: console.log(rtEvent); break; } }; // Start enroll process this.getUserByUserId(user_id) .then(async (user) => { /** First check if Finger index already exists, and if so, it must be deleted */ try { const exists = await this.DownloadFp(user_id, tempId); console.log("exists: ", exists); if (exists) { this._zkTcp.verbose && console.debug("Deleting Finger index before start enroll"); await this.deleteFinger(user_id, tempId); } } catch (e) { this._zkTcp.verbose && console.debug("Finger index is empty, skipping delete"); } const userBuf = Buffer.alloc(24); userBuf.write(user_id, 0, 24, "ascii"); const commandString = Buffer.concat([ userBuf, Buffer.from([tempId, 1]), ]); await this._zkTcp.executeCmd(COMMANDS.CMD_STARTENROLL, commandString); // #5 this._zkTcp.timeout = 60000; // 60 seconds timeout await this._zkTcp.executeCmd(COMMANDS.CMD_STARTVERIFY, ""); // #17 timer = setTimeoutTimer(reject); void (await this._zkTcp.getRealTimeLogs(handleRtEvent)); // #9 }) .catch(reject); }); } 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 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); if (initPacket.readUInt16LE(0) !== COMMANDS.CMD_ACK_OK) { throw new Error(`received unexpected command: ${initPacket.readUInt16LE(0)}`); } const fpPacket = await this._zkTcp.executeCmd(COMMANDS.CMD_DATA, fingerBuffer); if (fpPacket.readUInt16LE(0) !== COMMANDS.CMD_ACK_OK) { throw new Error(`received unexpected command: ${fpPacket.readUInt16LE(0)}`); } const cheksumPacket = await this._zkTcp.executeCmd(COMMANDS.CMD_CHECKSUM_BUFFER, ""); if (cheksumPacket.readUInt16LE(0) !== COMMANDS.CMD_ACK_OK) { throw new Error(`received unexpected command: ${cheksumPacket.readUInt16LE(0)}`); } 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); if (tmp_wreqPacket.readUInt16LE(0) !== COMMANDS.CMD_ACK_OK) { throw new Error(`received unexpected command: ${tmp_wreqPacket.readUInt16LE(0)}`); } const freeData = await this._zkTcp.executeCmd(COMMANDS.CMD_FREE_DATA, ""); if (freeData.readUInt16LE(0) !== COMMANDS.CMD_ACK_OK) { throw new Error(`received unexpected command: ${freeData.readUInt16LE(0)}`); } return true; } 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