zklib-ts
Version:
Unofficial zkteco library allows Node.js developers to easily interface with ZK BioMetric Fingerprint Attendance Devices
532 lines (524 loc) • 19 kB
JavaScript
'use strict';
var utils = require('./helper/utils');
var command = require('./helper/command');
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);
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, () => { });
};
/**
*
* @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 };
class ZUDP {
ip;
port;
timeout;
socket;
sessionId;
replyId;
inport;
comm_key;
constructor(ip, port, timeout, inport, comm_key = 0) {
this.ip = ip;
this.port = port;
this.timeout = timeout;
this.socket = null;
this.sessionId = null;
this.replyId = 0;
this.inport = inport;
this.comm_key = comm_key;
}
createSocket(cbError, cbClose) {
return new Promise((resolve, reject) => {
this.socket = dgram__namespace.createSocket('udp4');
this.socket.setMaxListeners(Infinity);
this.socket.once('error', (err) => {
this.socket = null;
reject(err);
if (cbError)
cbError(err);
});
this.socket.once('close', () => {
this.socket = null;
if (cbClose)
cbClose('udp');
});
this.socket.once('listening', () => {
resolve(this.socket);
});
try {
this.socket.bind(this.inport);
}
catch (err) {
this.socket = null;
reject(err);
if (cbError)
cbError(err);
}
});
}
async connect() {
try {
let reply = await this.executeCmd(command.COMMANDS.CMD_CONNECT, '');
if (reply.readUInt16LE(0) === command.COMMANDS.CMD_ACK_OK) {
return true;
}
if (reply.readUInt16LE(0) === command.COMMANDS.CMD_ACK_UNAUTH) {
const hashedCommkey = utils.authKey(this.comm_key, this.sessionId);
reply = await this.executeCmd(command.COMMANDS.CMD_AUTH, hashedCommkey);
if (reply.readUInt16LE(0) === command.COMMANDS.CMD_ACK_OK) {
return true;
}
else {
throw new Error('Authentication error');
}
}
else {
throw new Error('NO_REPLY_ON_CMD_CONNECT');
}
}
catch (err) {
console.error('Error in connect method:', err);
throw err;
}
}
async closeSocket() {
return new Promise((resolve, reject) => {
if (!this.socket) {
resolve(true);
return;
}
const timeout = 2000;
const timer = setTimeout(() => {
console.warn('Socket close timeout');
resolve(true);
}, timeout);
this.socket.removeAllListeners('message');
// @ts-ignore
this.socket.close((err) => {
clearTimeout(timer);
if (err) {
console.error('Error closing socket:', err);
reject(err);
}
else {
resolve(true);
}
this.socket = null;
});
});
}
writeMessage(msg, connect) {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
let sendTimeoutId;
const onMessage = (data) => {
clearTimeout(sendTimeoutId);
this.socket.removeListener('message', onMessage);
resolve(data);
};
this.socket.once('message', onMessage);
this.socket.send(msg, 0, msg.length, this.port, this.ip, (err) => {
if (err) {
this.socket.removeListener('message', onMessage);
reject(err);
return;
}
if (this.timeout) {
sendTimeoutId = setTimeout(() => {
this.socket.removeListener('message', onMessage);
reject(new Error('TIMEOUT_ON_WRITING_MESSAGE'));
}, connect ? 2000 : this.timeout);
}
});
});
}
requestData(msg) {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
let sendTimeoutId;
let responseTimeoutId;
const handleOnData = (data) => {
if (utils.checkNotEventUDP(data))
return;
clearTimeout(sendTimeoutId);
clearTimeout(responseTimeoutId);
this.socket.removeListener('message', handleOnData);
resolve(data);
};
const onReceiveTimeout = () => {
this.socket.removeListener('message', handleOnData);
reject(new Error('TIMEOUT_ON_RECEIVING_REQUEST_DATA'));
};
this.socket.on('message', handleOnData);
this.socket.send(msg, 0, msg.length, this.port, this.ip, (err) => {
if (err) {
this.socket.removeListener('message', handleOnData);
reject(err);
return;
}
responseTimeoutId = setTimeout(onReceiveTimeout, this.timeout);
});
sendTimeoutId = setTimeout(() => {
this.socket.removeListener('message', handleOnData);
reject(new Error('TIMEOUT_IN_RECEIVING_RESPONSE_AFTER_REQUESTING_DATA'));
}, this.timeout);
});
}
async executeCmd(command$1, data) {
try {
if (command$1 === command.COMMANDS.CMD_CONNECT) {
this.sessionId = 0;
this.replyId = 0;
}
else {
this.replyId++;
}
const buf = utils.createUDPHeader(command$1, this.sessionId, this.replyId, data);
const reply = await this.writeMessage(buf, command$1 === command.COMMANDS.CMD_CONNECT || command$1 === command.COMMANDS.CMD_EXIT);
if (reply && reply.length > 0) {
if (command$1 === command.COMMANDS.CMD_CONNECT) {
this.sessionId = reply.readUInt16LE(4);
}
}
return reply;
}
catch (err) {
console.error(`Error executing command ${command$1}:`, err);
throw err;
}
}
async sendChunkRequest(start, size) {
this.replyId++;
const reqData = Buffer.alloc(8);
reqData.writeUInt32LE(start, 0);
reqData.writeUInt32LE(size, 4);
const buf = utils.createUDPHeader(command.COMMANDS.CMD_DATA_RDY, this.sessionId, this.replyId, reqData);
try {
await new Promise((resolve, reject) => {
this.socket.send(buf, 0, buf.length, this.port, this.ip, (err) => {
if (err) {
log(`[UDP][SEND_CHUNK_REQUEST] Error sending chunk request: ${err.message}`);
reject(err);
}
else {
resolve();
}
});
});
}
catch (error) {
log(`[UDP][SEND_CHUNK_REQUEST] Exception: ${error.message}`);
throw error;
}
}
async readWithBuffer(reqData, cb = null) {
this.replyId++;
const buf = utils.createUDPHeader(command.COMMANDS.CMD_DATA_WRRQ, this.sessionId, this.replyId, reqData);
try {
const reply = await this.requestData(buf);
const header = utils.decodeUDPHeader(reply.subarray(0, 8));
switch (header.commandId) {
case command.COMMANDS.CMD_DATA:
return { data: reply.subarray(8), err: null };
case command.COMMANDS.CMD_ACK_OK:
case command.COMMANDS.CMD_PREPARE_DATA:
return await this.handleChunkedData(reply, header.commandId, cb);
default:
throw new Error('ERROR_IN_UNHANDLE_CMD ' + utils.exportErrorMessage(header.commandId));
}
}
catch (err) {
return { data: null, err: err };
}
}
async handleChunkedData(reply, commandId, cb) {
return new Promise((resolve) => {
const recvData = reply.subarray(8);
const size = recvData.readUIntLE(1, 4);
let totalBuffer = Buffer.from([]);
const timeout = 3000;
let timer = setTimeout(() => {
this.socket.removeListener('message', handleOnData);
resolve({ data: null, err: new Error('TIMEOUT WHEN RECEIVING PACKET') });
}, timeout);
const internalCallback = (replyData, err = null) => {
this.socket.removeListener('message', handleOnData);
clearTimeout(timer);
resolve({ data: err ? null : replyData, err });
};
const handleOnData = (reply) => {
if (utils.checkNotEventUDP(reply))
return;
clearTimeout(timer);
timer = setTimeout(() => {
internalCallback(totalBuffer, new Error(`TIMEOUT !! ${(size - totalBuffer.length) / size} % REMAIN !`));
}, timeout);
const header = utils.decodeUDPHeader(reply);
switch (header.commandId) {
case command.COMMANDS.CMD_PREPARE_DATA:
break;
case command.COMMANDS.CMD_DATA:
totalBuffer = Buffer.concat([totalBuffer, reply.subarray(8)]);
cb && cb(totalBuffer.length, size);
break;
case command.COMMANDS.CMD_ACK_OK:
if (totalBuffer.length === size) {
internalCallback(totalBuffer);
}
break;
default:
internalCallback(Buffer.from([]), new Error('ERROR_IN_UNHANDLE_CMD ' + utils.exportErrorMessage(header.commandId)));
}
};
this.socket.on('message', handleOnData);
const chunkCount = Math.ceil(size / command.Constants.MAX_CHUNK);
for (let i = 0; i < chunkCount; i++) {
const start = i * command.Constants.MAX_CHUNK;
const chunkSize = (i === chunkCount - 1) ? size % command.Constants.MAX_CHUNK : command.Constants.MAX_CHUNK;
this.sendChunkRequest(start, chunkSize).catch(err => {
internalCallback(Buffer.from([]), err);
});
}
});
}
async getUsers() {
try {
if (this.socket) {
await this.freeData();
}
const data = await this.readWithBuffer(command.REQUEST_DATA.GET_USERS);
if (this.socket) {
await this.freeData();
}
const USER_PACKET_SIZE = 28;
let userData = data.data?.subarray(4) || Buffer.from([]);
const users = [];
while (userData.length >= USER_PACKET_SIZE) {
const user = utils.decodeUserData28(userData.subarray(0, USER_PACKET_SIZE));
users.push(user);
userData = userData.subarray(USER_PACKET_SIZE);
}
return { data: users };
}
catch (err) {
throw new Error(err.message);
}
}
async getAttendances(callbackInProcess) {
try {
if (this.socket) {
await this.freeData();
}
const data = await this.readWithBuffer(command.REQUEST_DATA.GET_ATTENDANCE_LOGS);
if (this.socket) {
await this.freeData();
}
const RECORD_PACKET_SIZE = 16;
let recordData = data.data?.subarray(4) || Buffer.from([]);
const records = [];
while (recordData.length >= RECORD_PACKET_SIZE) {
const record = utils.decodeRecordData16(recordData.subarray(0, RECORD_PACKET_SIZE));
records.push({ ...record, ip: this.ip });
recordData = recordData.subarray(RECORD_PACKET_SIZE);
}
return { data: records, err: data.err };
}
catch (err) {
return { data: [], err: err };
}
}
async freeData() {
try {
const resp = await this.executeCmd(command.COMMANDS.CMD_FREE_DATA, Buffer.alloc(0));
return !!resp;
}
catch (err) {
console.error('Error freeing data:', err);
throw err;
}
}
async getInfo() {
try {
const data = await this.executeCmd(command.COMMANDS.CMD_GET_FREE_SIZES, Buffer.alloc(0));
return {
userCounts: data.readUIntLE(24, 4),
logCounts: data.readUIntLE(40, 4),
logCapacity: data.readUIntLE(72, 4)
};
}
catch (err) {
console.error('Error retrieving info:', err);
throw err;
}
}
async getTime() {
try {
const response = await this.executeCmd(command.COMMANDS.CMD_GET_TIME, Buffer.alloc(0));
const timeValue = response.readUInt32LE(8);
return timeParser.decode(timeValue);
}
catch (err) {
console.error('Error retrieving time:', err);
throw err;
}
}
async setTime(tm) {
try {
const commandBuffer = Buffer.alloc(32);
commandBuffer.writeUInt32LE(timeParser.encode(new Date(tm)), 0);
await this.executeCmd(command.COMMANDS.CMD_SET_TIME, commandBuffer);
return true;
}
catch (err) {
console.error('Error setting time:', err);
throw err;
}
}
async clearAttendanceLog() {
try {
return !!await this.executeCmd(command.COMMANDS.CMD_CLEAR_ATTLOG, Buffer.alloc(0));
}
catch (err) {
console.error('Error clearing attendance log:', err);
throw err;
}
}
async clearData() {
try {
return !!await this.executeCmd(command.COMMANDS.CMD_CLEAR_DATA, Buffer.alloc(0));
}
catch (err) {
console.error('Error clearing data:', err);
throw err;
}
}
async disableDevice() {
try {
const resp = await this.executeCmd(command.COMMANDS.CMD_DISABLEDEVICE, command.REQUEST_DATA.DISABLE_DEVICE);
return !!resp;
}
catch (err) {
console.error('Error disabling device:', err);
throw err;
}
}
async enableDevice() {
try {
const resp = await this.executeCmd(command.COMMANDS.CMD_ENABLEDEVICE, Buffer.alloc(0));
return !!resp;
}
catch (err) {
console.error('Error enabling device:', err);
throw err;
}
}
async disconnect() {
try {
await this.executeCmd(command.COMMANDS.CMD_EXIT, Buffer.alloc(0));
}
catch (err) {
console.error('Error executing disconnect command:', err);
}
try {
await this.closeSocket();
}
catch (err) {
console.error('Error closing the socket:', err);
}
}
async getRealTimeLogs(cb = () => { }) {
this.replyId++;
const buf = utils.createUDPHeader(command.COMMANDS.CMD_REG_EVENT, this.sessionId, this.replyId, command.REQUEST_DATA.GET_REAL_TIME_EVENT);
try {
this.socket.send(buf, 0, buf.length, this.port, this.ip, (err) => {
if (err) {
console.error('Error sending UDP message:', err);
return;
}
console.log('UDP message sent successfully');
});
}
catch (err) {
console.error('Error during send operation:', err);
return;
}
const handleMessage = (data) => {
if (!utils.checkNotEventUDP(data))
return;
if (data.length === 18) {
cb(utils.decodeRecordRealTimeLog18(data));
}
};
if (this.socket.listenerCount('message') === 0) {
this.socket.on('message', handleMessage);
}
else {
console.warn('Multiple message listeners detected. Ensure only one listener is attached.');
}
}
}
exports.ZUDP = ZUDP;