zkteco-js
Version:
The zkteco library allows Node.js developers to easily interface with ZK BioMetric Fingerprint Attendance Devices, extract and manage data, and integrate biometric features into attendance systems efficiently.
616 lines (511 loc) • 22.1 kB
JavaScript
/**
*
* Author: coding-libs
* Date: 2024-07-01
*/
const dgram = require('dgram')
const {
createUDPHeader,
decodeUserData28,
decodeRecordData16,
decodeRecordRealTimeLog18,
decodeUDPHeader,
exportErrorMessage,
checkNotEventUDP
} = require('./helper/utils')
const {MAX_CHUNK, REQUEST_DATA, COMMANDS} = require('./helper/command')
const { log } = require('./logs/log')
const timeParser = require("./helper/time");
class ZUDP {
constructor(ip, port, timeout, inport) {
this.ip = ip
this.port = port
this.timeout = timeout
this.socket = null
this.sessionId = null
this.replyId = 0
this.inport = inport
}
createSocket(cbError = null, cbClose = null) {
return new Promise((resolve, reject) => {
this.socket = dgram.createSocket('udp4');
this.socket.setMaxListeners(Infinity); // Allow unlimited listeners
// Handle socket errors
this.socket.once('error', err => {
this.socket = null; // Clean up the socket reference
reject(err); // Reject the promise
if (cbError) cbError(err); // Call the error callback if provided
});
// Handle socket close event
this.socket.once('close', () => {
this.socket = null; // Clean up the socket reference
if (cbClose) cbClose('udp'); // Call the close callback if provided
});
// Handle socket listening event
this.socket.once('listening', () => {
resolve(this.socket); // Resolve the promise with the socket
});
// Bind the socket to the specified port
try {
this.socket.bind(this.inport);
} catch (err) {
this.socket = null; // Clean up the socket reference
reject(err); // Reject the promise
if (cbError) cbError(err); // Call the error callback if provided
}
});
}
async connect() {
try {
const reply = await this.executeCmd(COMMANDS.CMD_CONNECT, '');
if (reply) {
return true; // Resolve with true if the reply is valid
} else {
throw new Error('NO_REPLY_ON_CMD_CONNECT'); // Throw an error if no reply
}
} catch (err) {
// Log the error for debugging purposes
console.error('Error in connect method:', err);
throw err; // Re-throw the error to be handled by the caller
}
}
closeSocket() {
return new Promise((resolve, reject) => {
// Ensure the socket exists and is properly handled
if (!this.socket) {
resolve(true);
return;
}
// Remove all listeners for the 'message' event
this.socket.removeAllListeners('message');
// Create a timeout to handle cases where the socket might not close in a timely manner
const timeout = 2000; // Timeout duration in milliseconds
const timer = setTimeout(() => {
console.warn('Socket close timeout');
resolve(true);
}, timeout);
// Handle the socket close operation
this.socket.close((err) => {
// Clear the timer as socket is closing
clearTimeout(timer);
// Handle any potential errors during the closing process
if (err) {
console.error('Error closing socket:', err);
reject(err);
} else {
resolve(true);
}
// Set the socket to null after closing
this.socket = null;
});
});
}
writeMessage(msg, connect) {
return new Promise((resolve, reject) => {
let sendTimeoutId;
// Setup a listener for the response message
const onMessage = (data) => {
clearTimeout(sendTimeoutId); // Clear timeout if message is received
this.socket.removeListener('message', onMessage); // Remove the listener
resolve(data); // Resolve the promise with the received data
};
this.socket.once('message', onMessage); // Use once to ensure single response
// Send the message
this.socket.send(msg, 0, msg.length, this.port, this.ip, (err) => {
if (err) {
this.socket.removeListener('message', onMessage); // Clean up listener on error
reject(err); // Reject the promise with the error
return; // Exit early to avoid setting timeout on error
}
// Setup a timeout if a timeout duration is specified
if (this.timeout) {
sendTimeoutId = setTimeout(() => {
this.socket.removeListener('message', onMessage); // Clean up listener on timeout
reject(new Error('TIMEOUT_ON_WRITING_MESSAGE')); // Reject the promise on timeout
}, connect ? 2000 : this.timeout);
}
});
});
}
requestData(msg) {
return new Promise((resolve, reject) => {
let sendTimeoutId;
let responseTimeoutId;
// Define the callback to handle incoming data
const handleOnData = (data) => {
if (checkNotEventUDP(data)) return; // Filter out unwanted data
// Clear any existing timeouts
clearTimeout(sendTimeoutId);
clearTimeout(responseTimeoutId);
// Remove the event listener for 'message'
this.socket.removeListener('message', handleOnData);
// Resolve the promise with the received data
resolve(data);
};
// Define the timeout callback for handling the receive timeout
const onReceiveTimeout = () => {
this.socket.removeListener('message', handleOnData);
reject(new Error('TIMEOUT_ON_RECEIVING_REQUEST_DATA'));
};
// Attach the data event listener
this.socket.on('message', handleOnData);
// Send the message
this.socket.send(msg, 0, msg.length, this.port, this.ip, (err) => {
if (err) {
this.socket.removeListener('message', handleOnData); // Clean up listener on error
reject(err);
return;
}
// Set up the timeout for receiving a response
responseTimeoutId = setTimeout(onReceiveTimeout, this.timeout);
});
// Set up the timeout for sending the message
sendTimeoutId = setTimeout(() => {
this.socket.removeListener('message', handleOnData);
reject(new Error('TIMEOUT_IN_RECEIVING_RESPONSE_AFTER_REQUESTING_DATA'));
}, this.timeout);
});
}
/**
*
* @param {*} command
* @param {*} data
*
*
* reject error when command fail and resolve data when success
*/
async executeCmd(command, data) {
try {
// Handle command-specific logic
if (command === COMMANDS.CMD_CONNECT) {
this.sessionId = 0;
this.replyId = 0;
} else {
this.replyId++;
}
// Create and send the UDP packet
const buf = createUDPHeader(command, this.sessionId, this.replyId, data);
const reply = await this.writeMessage(buf, command === COMMANDS.CMD_CONNECT || command === COMMANDS.CMD_EXIT);
// Process the reply if necessary
if (reply && reply.length > 0) { // Check if reply is not empty
if (command === COMMANDS.CMD_CONNECT) {
this.sessionId = reply.readUInt16LE(4);
}
}
// Return the reply
return reply;
} catch (err) {
// Handle errors by logging or throwing them
console.error(`Error executing command ${command}:`, err);
throw err;
}
}
async sendChunkRequest(start, size) {
this.replyId++;
const reqData = Buffer.alloc(8);
reqData.writeUInt32LE(start, 0);
reqData.writeUInt32LE(size, 4);
const buf = createUDPHeader(COMMANDS.CMD_DATA_RDY, this.sessionId, this.replyId, reqData);
try {
await new Promise((resolve, reject) => {
// Send the buffer over UDP
this.socket.send(buf, 0, buf.length, this.port, this.ip, (err) => {
if (err) {
// Log the error and reject the promise
log(`[UDP][SEND_CHUNK_REQUEST] Error sending chunk request: ${err.message}`);
reject(err);
} else {
// Resolve the promise if sending was successful
resolve();
}
});
});
} catch (error) {
// Log any exceptions that occur during the send operation
log(`[UDP][SEND_CHUNK_REQUEST] Exception: ${error.message}`);
throw error; // Re-throw the error if it needs to be handled further up
}
}
/**
*
* @param {*} reqData - indicate the type of data that need to receive ( user or attLog)
* @param {*} cb - callback is triggered when receiving packets
*
* readWithBuffer will reject error if it'wrong when starting request data
* readWithBuffer will return { data: replyData , err: Error } when receiving requested data
*/
async readWithBuffer(reqData, cb = null) {
this.replyId++;
const buf = createUDPHeader(COMMANDS.CMD_DATA_WRRQ, this.sessionId, this.replyId, reqData);
try {
const reply = await this.requestData(buf);
const header = decodeUDPHeader(reply.subarray(0, 8));
switch (header.commandId) {
case COMMANDS.CMD_DATA:
return { data: reply.subarray(8), mode: 8, err: null };
case COMMANDS.CMD_ACK_OK:
case COMMANDS.CMD_PREPARE_DATA:
return await this.handleChunkedData(reply, header.commandId, cb);
default:
throw new Error('ERROR_IN_UNHANDLE_CMD ' + exportErrorMessage(header.commandId));
}
} catch (err) {
return { err, data: null };
}
}
async handleChunkedData(reply, commandId, cb) {
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);
throw new Error('TIMEOUT WHEN RECEIVING PACKET');
}, timeout);
const internalCallback = (replyData, err = null) => {
this.socket.removeListener('message', handleOnData);
clearTimeout(timer);
if (err) {
return { err, data: replyData };
}
return { err: null, data: replyData };
};
const handleOnData = (reply) => {
if (checkNotEventUDP(reply)) return;
clearTimeout(timer);
timer = setTimeout(() => {
internalCallback(totalBuffer, new Error(`TIMEOUT !! ${(size - totalBuffer.length) / size} % REMAIN !`));
}, timeout);
const header = decodeUDPHeader(reply);
switch (header.commandId) {
case COMMANDS.CMD_PREPARE_DATA:
break;
case COMMANDS.CMD_DATA:
totalBuffer = Buffer.concat([totalBuffer, reply.subarray(8)]);
cb && cb(totalBuffer.length, size);
break;
case COMMANDS.CMD_ACK_OK:
if (totalBuffer.length === size) {
internalCallback(totalBuffer);
}
break;
default:
internalCallback([], new Error('ERROR_IN_UNHANDLE_CMD ' + exportErrorMessage(header.commandId)));
}
};
this.socket.on('message', handleOnData);
const chunkCount = Math.ceil(size / MAX_CHUNK);
for (let i = 0; i < chunkCount; i++) {
const start = i * MAX_CHUNK;
const chunkSize = (i === chunkCount - 1) ? size % MAX_CHUNK : MAX_CHUNK;
this.sendChunkRequest(start, chunkSize);
}
}
async getUsers() {
try {
// Free Buffer Data to request Data
if (this.socket) {
await this.freeData();
}
// Read user data from the buffer
const data = await this.readWithBuffer(REQUEST_DATA.GET_USERS);
// Free Buffer Data after requesting data
if (this.socket) {
await this.freeData();
}
const USER_PACKET_SIZE = 28;
let userData = data.data.subarray(4);
const users = [];
// Decode user data
while (userData.length >= USER_PACKET_SIZE) {
const user = decodeUserData28(userData.subarray(0, USER_PACKET_SIZE));
users.push(user);
userData = userData.subarray(USER_PACKET_SIZE);
}
return { data: users, err: data.err };
} catch (err) {
// Handle any errors that occurred
return { data: [], err };
}
}
/**
*
* @param {*} ip
* @param {*} callbackInProcess
* reject error when starting request data
* return { data: records, err: Error } when receiving requested data
*/
async getAttendances(callbackInProcess = () => {}) {
try {
// Free Buffer Data before requesting new data
if (this.socket) {
await this.freeData();
}
// Read attendance data
const data = await this.readWithBuffer(REQUEST_DATA.GET_ATTENDANCE_LOGS, callbackInProcess);
// Free Buffer Data after requesting data
if (this.socket) {
await this.freeData();
}
const RECORD_PACKET_SIZE = data.mode ? 8 : 16;
let recordData = data.data.subarray(4);
// Process and decode record data
const records = [];
while (recordData.length >= RECORD_PACKET_SIZE) {
const record = 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) {
// Handle errors that occurred during the process
return { data: [], err };
}
}
async freeData() {
try {
// Send command to free data with an empty buffer
return await this.executeCmd(COMMANDS.CMD_FREE_DATA, Buffer.alloc(0));
} catch (err) {
// Handle errors and rethrow or log if necessary
console.error('Error freeing data:', err);
throw err; // Re-throw the error to propagate it
}
}
async getInfo() {
try {
// Execute command to get free sizes
const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, Buffer.alloc(0));
// Parse the data
return {
userCounts: data.readUIntLE(24, 4),
logCounts: data.readUIntLE(40, 4),
logCapacity: data.readUIntLE(72, 4)
};
} catch (err) {
// Handle and propagate any errors that occur
console.error('Error retrieving info:', err);
throw err; // Re-throw the error to allow it to be handled by the caller
}
}
async getTime() {
try {
// Execute command to get time
const response = await this.executeCmd(COMMANDS.CMD_GET_TIME, Buffer.alloc(0));
// Parse and return the time
const timeValue = response.readUInt32LE(8);
return timeParser.decode(timeValue);
} catch (err) {
// Log and propagate the error
console.error('Error retrieving time:', err);
throw err; // Re-throw the error to be handled by the caller
}
}
async setTime(tm) {
try {
// Create a buffer for the command
const commandBuffer = Buffer.alloc(32);
// Encode the time and write it to the buffer
commandBuffer.writeUInt32LE(timeParser.encode(new Date(tm)), 0);
// Send the command to set the time
await this.executeCmd(COMMANDS.CMD_SET_TIME, commandBuffer);
// Indicate success
return true;
} catch (err) {
// Log and propagate the error
console.error('Error setting time:', err);
throw err; // Re-throw the error to allow it to be handled by the caller
}
}
async clearAttendanceLog() {
try {
// Execute command to clear attendance log
return await this.executeCmd(COMMANDS.CMD_CLEAR_ATTLOG, Buffer.alloc(0));
} catch (err) {
// Log and propagate the error
console.error('Error clearing attendance log:', err);
throw err; // Re-throw the error to allow it to be handled by the caller
}
}
async clearData() {
try {
// Execute command to clear data
return await this.executeCmd(COMMANDS.CMD_CLEAR_DATA, Buffer.alloc(0));
} catch (err) {
// Log and propagate the error
console.error('Error clearing data:', err);
throw err; // Re-throw the error to allow it to be handled by the caller
}
}
async disableDevice() {
try {
// Execute command to disable the device with required data
return await this.executeCmd(COMMANDS.CMD_DISABLEDEVICE, REQUEST_DATA.DISABLE_DEVICE);
} catch (err) {
// Log and propagate the error
console.error('Error disabling device:', err);
throw err; // Re-throw the error to allow it to be handled by the caller
}
}
async enableDevice() {
try {
// Execute command to enable the device
return await this.executeCmd(COMMANDS.CMD_ENABLEDEVICE, Buffer.alloc(0));
} catch (err) {
// Log and propagate the error
console.error('Error enabling device:', err);
throw err; // Re-throw the error to allow it to be handled by the caller
}
}
async disconnect() {
try {
// Attempt to send the disconnect command
await this.executeCmd(COMMANDS.CMD_EXIT, Buffer.alloc(0));
} catch (err) {
// Log the error if the command fails
console.error('Error executing disconnect command:', err);
// Optionally, you can handle the error or clean up here
}
// Ensure the socket is closed
try {
await this.closeSocket();
} catch (err) {
// Log the error if closing the socket fails
console.error('Error closing the socket:', err);
// Optionally, you can handle the error or clean up here
}
}
async getRealTimeLogs(cb = () => {}) {
// Increment replyId
this.replyId++;
// Create the UDP header with the command and data
const buf = createUDPHeader(COMMANDS.CMD_REG_EVENT, this.sessionId, this.replyId, REQUEST_DATA.GET_REAL_TIME_EVENT);
// Send the command via the socket
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; // Early return if sending fails
}
// Add a single listener for the 'message' event
const handleMessage = (data) => {
if (!checkNotEventUDP(data)) return;
if (data.length === 18) {
cb(decodeRecordRealTimeLog18(data));
}
};
if (this.socket.listenerCount('message') === 0) {
this.socket.on('message', handleMessage);
} else {
// Optionally handle the case where multiple listeners are not allowed
console.warn('Multiple message listeners detected. Ensure only one listener is attached.');
}
}
}
module.exports = ZUDP