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.
1,107 lines (899 loc) • 42.6 kB
JavaScript
/**
*
* Author: coding-libs
* Date: 2024-07-01
*/
const net = require('net')
const {MAX_CHUNK, COMMANDS, REQUEST_DATA} = require('./helper/command')
const timeParser = require('./helper/time');
const {
createTCPHeader,
exportErrorMessage,
removeTcpHeader,
decodeUserData72,
decodeRecordData40,
decodeRecordRealTimeLog52,
checkNotEventTCP,
decodeTCPHeader
} = require('./helper/utils')
const {log} = require('./logs/log')
const {error} = require('console')
class ZTCP {
constructor(ip, port, timeout) {
this.ip = ip;
this.port = port;
this.timeout = timeout;
this.sessionId = null;
this.replyId = 0;
this.socket = null;
}
createSocket(cbError, cbClose) {
return new Promise((resolve, reject) => {
this.socket = new net.Socket();
// Handle socket error
this.socket.once('error', (err) => {
this.socket = null; // Ensure socket reference is cleared
reject(err);
if (typeof cbError === 'function') cbError(err);
});
// Handle successful connection
this.socket.once('connect', () => {
resolve(this.socket);
});
// Handle socket closure
this.socket.once('close', () => {
this.socket = null; // Ensure socket reference is cleared
if (typeof cbClose === 'function') cbClose('tcp');
});
// Set socket timeout if provided
if (this.timeout) {
this.socket.setTimeout(this.timeout);
}
// Initiate connection
this.socket.connect(this.port, this.ip);
});
}
async connect() {
try {
const reply = await this.executeCmd(COMMANDS.CMD_CONNECT, '');
if (reply) {
// Connection successful
return true;
} else {
// No reply received; throw an error
throw new Error('NO_REPLY_ON_CMD_CONNECT');
}
} catch (err) {
// Log the error for debugging, if necessary
console.error('Failed to connect:', err);
// Re-throw the error for handling by the caller
throw err;
}
}
async closeSocket() {
return new Promise((resolve, reject) => {
// If no socket is present, resolve immediately
if (!this.socket) {
return resolve(true);
}
// Clean up listeners to avoid potential memory leaks or duplicate handling
this.socket.removeAllListeners('data');
// Set a timeout to handle cases where socket.end might not resolve
const timer = setTimeout(() => {
this.socket.destroy(); // Forcibly close the socket if not closed properly
resolve(true); // Resolve even if the socket was not closed properly
}, 2000);
// Close the socket and clear the timeout upon successful completion
this.socket.end(() => {
clearTimeout(timer);
resolve(true); // Resolve once the socket has ended
});
// Handle socket errors during closing
this.socket.once('error', (err) => {
clearTimeout(timer);
reject(err); // Reject the promise with the error
});
});
}
writeMessage(msg, connect) {
return new Promise((resolve, reject) => {
// Check if the socket is initialized
if (!this.socket) {
return reject(new Error('Socket is not initialized'));
}
// Define a variable for the timeout reference
let timer = null;
// Handle incoming data
const onData = (data) => {
// Check if the socket is still valid before trying to remove the listener
if (this.socket) {
this.socket.removeListener('data', onData); // Remove the data event listener
}
clearTimeout(timer); // Clear the timeout once data is received
resolve(data); // Resolve the promise with the received data
};
// Attach the data event listener
this.socket.once('data', onData);
// Attempt to write the message to the socket
this.socket.write(msg, null, (err) => {
if (err) {
// Check if the socket is still valid before trying to remove the listener
if (this.socket) {
this.socket.removeListener('data', onData); // Clean up listener on write error
}
return reject(err); // Reject the promise with the write error
}
// If a timeout is set, configure it
if (this.timeout) {
timer = setTimeout(() => {
// Check if the socket is still valid before trying to remove the listener
if (this.socket) {
this.socket.removeListener('data', onData); // Remove 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 timer = null;
let replyBuffer = Buffer.from([]);
// Internal callback to handle data reception
const internalCallback = (data) => {
if (this.socket) {
this.socket.removeListener('data', handleOnData); // Clean up listener
}
if (timer) clearTimeout(timer); // Clear the timeout
resolve(data); // Resolve the promise with the data
};
// Handle incoming data
const handleOnData = (data) => {
replyBuffer = Buffer.concat([replyBuffer, data]); // Accumulate data
// Check if the data is a valid TCP event
if (checkNotEventTCP(data)) return;
// Decode the TCP header
const header = decodeTCPHeader(replyBuffer.subarray(0, 16));
// Handle based on command ID
if (header.commandId === COMMANDS.CMD_DATA) {
// Set a timeout to handle delayed responses
timer = setTimeout(() => {
internalCallback(replyBuffer); // Resolve with accumulated buffer
}, 1000);
} else {
// Set a timeout to handle errors
timer = setTimeout(() => {
if (this.socket) {
this.socket.removeListener('data', handleOnData); // Clean up listener on timeout
}
reject(new Error('TIMEOUT_ON_RECEIVING_REQUEST_DATA')); // Reject on timeout
}, this.timeout);
// Extract packet length and handle accordingly
const packetLength = data.readUIntLE(4, 2);
if (packetLength > 8) {
internalCallback(data); // Resolve immediately if sufficient data
}
}
};
// Ensure the socket is valid before attaching the listener
if (this.socket) {
this.socket.on('data', handleOnData);
// Write the message to the socket
this.socket.write(msg, null, (err) => {
if (err) {
if (this.socket) {
this.socket.removeListener('data', handleOnData); // Clean up listener on error
}
return reject(err); // Reject the promise with the error
}
// Set a timeout to handle cases where no response is received
timer = setTimeout(() => {
if (this.socket) {
this.socket.removeListener('data', handleOnData); // Clean up listener on timeout
}
reject(new Error('TIMEOUT_IN_RECEIVING_RESPONSE_AFTER_REQUESTING_DATA')); // Reject on timeout
}, this.timeout);
});
} else {
reject(new Error('SOCKET_NOT_INITIALIZED')); // Reject if socket is not initialized
}
}).catch((err) => {
console.error("Promise Rejected:", err); // Log the rejection reason
throw err; // Re-throw the error to be handled by the caller
});
}
/**
*
* @param {*} command
* @param {*} data
*
*
* reject error when command fail and resolve data when success
*/
async executeCmd(command, data) {
// Reset sessionId and replyId for connection commands
if (command === COMMANDS.CMD_CONNECT) {
this.sessionId = 0;
this.replyId = 0;
} else {
this.replyId++;
}
const buf = createTCPHeader(command, this.sessionId, this.replyId, data);
try {
// Write the message to the socket and wait for a response
const reply = await this.writeMessage(buf, command === COMMANDS.CMD_CONNECT || command === COMMANDS.CMD_EXIT);
// Remove TCP header from the response
const rReply = removeTcpHeader(reply);
// Update sessionId for connection command responses
if (command === COMMANDS.CMD_CONNECT && rReply && rReply.length >= 6) { // Assuming sessionId is located at offset 4 and is 2 bytes long
this.sessionId = rReply.readUInt16LE(4);
}
return rReply;
} catch (err) {
// Log or handle the error if necessary
console.error('Error executing command:', err);
throw err; // Re-throw the error for handling by the caller
}
}
async sendChunkRequest(start, size) {
this.replyId++;
const reqData = Buffer.alloc(8);
reqData.writeUInt32LE(start, 0);
reqData.writeUInt32LE(size, 4);
const buf = createTCPHeader(COMMANDS.CMD_DATA_RDY, this.sessionId, this.replyId, reqData);
try {
await new Promise((resolve, reject) => {
this.socket.write(buf, null, (err) => {
if (err) {
console.error(`[TCP][SEND_CHUNK_REQUEST] Error sending chunk request: ${err.message}`);
reject(err); // Reject the promise if there is an error
} else {
resolve(); // Resolve the promise if the write operation succeeds
}
});
});
} catch (err) {
// Handle or log the error as needed
console.error(`[TCP][SEND_CHUNK_REQUEST] Exception: ${err.message}`);
throw err; // Re-throw the error for handling by the caller
}
}
/**
*
* @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
*/
readWithBuffer(reqData, cb = null) {
return new Promise(async (resolve, reject) => {
this.replyId++;
const buf = createTCPHeader(COMMANDS.CMD_DATA_WRRQ, this.sessionId, this.replyId, reqData)
let reply = null;
try {
reply = await this.requestData(buf)
//console.log(reply.toString('hex'));
} catch (err) {
reject(err)
console.log(reply)
}
const header = decodeTCPHeader(reply.subarray(0, 16))
switch (header.commandId) {
case COMMANDS.CMD_DATA: {
resolve({data: reply.subarray(16), mode: 8})
break;
}
case COMMANDS.CMD_ACK_OK:
case COMMANDS.CMD_PREPARE_DATA: {
// this case show that data is prepared => send command to get these data
// reply variable includes information about the size of following data
const recvData = reply.subarray(16)
const size = recvData.readUIntLE(1, 4)
// We need to split the data to many chunks to receive , because it's to large
// After receiving all chunk data , we concat it to TotalBuffer variable , that 's the data we want
let remain = size % MAX_CHUNK
let numberChunks = Math.round(size - remain) / MAX_CHUNK
let totalPackets = numberChunks + (remain > 0 ? 1 : 0)
let replyData = Buffer.from([])
let totalBuffer = Buffer.from([])
let realTotalBuffer = Buffer.from([])
const timeout = 10000
let timer = setTimeout(() => {
internalCallback(replyData, new Error('TIMEOUT WHEN RECEIVING PACKET'))
}, timeout)
const internalCallback = (replyData, err = null) => {
// this.socket && this.socket.removeListener('data', handleOnData)
timer && clearTimeout(timer)
resolve({data: replyData, err})
}
const handleOnData = (reply) => {
if (checkNotEventTCP(reply)) return;
clearTimeout(timer)
timer = setTimeout(() => {
internalCallback(replyData,
new Error(`TIME OUT !! ${totalPackets} PACKETS REMAIN !`))
}, timeout)
totalBuffer = Buffer.concat([totalBuffer, reply])
const packetLength = totalBuffer.readUIntLE(4, 2)
if (totalBuffer.length >= 8 + packetLength) {
realTotalBuffer = Buffer.concat([realTotalBuffer, totalBuffer.subarray(16, 8 + packetLength)])
totalBuffer = totalBuffer.subarray(8 + packetLength)
if ((totalPackets > 1 && realTotalBuffer.length === MAX_CHUNK + 8)
|| (totalPackets === 1 && realTotalBuffer.length === remain + 8)) {
replyData = Buffer.concat([replyData, realTotalBuffer.subarray(8)])
totalBuffer = Buffer.from([])
realTotalBuffer = Buffer.from([])
totalPackets -= 1
cb && cb(replyData.length, size)
if (totalPackets <= 0) {
internalCallback(replyData)
}
}
}
}
this.socket.once('close', () => {
internalCallback(replyData, new Error('Socket is disconnected unexpectedly'))
})
this.socket.on('data', handleOnData);
for (let i = 0; i <= numberChunks; i++) {
if (i === numberChunks) {
this.sendChunkRequest(numberChunks * MAX_CHUNK, remain)
} else {
this.sendChunkRequest(i * MAX_CHUNK, MAX_CHUNK)
}
}
break;
}
default: {
reject(new Error('ERROR_IN_UNHANDLE_CMD ' + exportErrorMessage(header.commandId)))
}
}
})
}
/**
* reject error when starting request data
* return { data: users, err: Error } when receiving requested data
*/
async getUsers() {
try {
// Free any existing buffer data to prepare for a new request
if (this.socket) {
await this.freeData();
}
// Request user data
const data = await this.readWithBuffer(REQUEST_DATA.GET_USERS);
// Free buffer data after receiving the data
if (this.socket) {
await this.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);
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;
}
}
/**
*
* @param {*} ip
* @param {*} callbackInProcess
* reject error when starting request data
* return { data: records, err: Error } when receiving requested data
*/
async getAttendances(callbackInProcess = () => {}) {
try {
// Free any existing buffer data to prepare for a new request
if (this.socket) {
await this.freeData();
}
// Request attendance logs and handle chunked data
const data = await this.readWithBuffer(REQUEST_DATA.GET_ATTENDANCE_LOGS, callbackInProcess);
// Free buffer data after receiving the attendance logs
if (this.socket) {
await this.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.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
}
}
async freeData() {
try {
return await this.executeCmd(COMMANDS.CMD_FREE_DATA, '');
} catch (err) {
console.error('Error freeing data:', err);
throw err; // Optionally, re-throw the error if you need to handle it upstream
}
}
async disableDevice() {
try {
return await this.executeCmd(COMMANDS.CMD_DISABLEDEVICE, REQUEST_DATA.DISABLE_DEVICE);
} catch (err) {
console.error('Error disabling device:', err);
throw err; // Optionally, re-throw the error if you need to handle it upstream
}
}
async enableDevice() {
try {
return await this.executeCmd(COMMANDS.CMD_ENABLEDEVICE, '');
} catch (err) {
console.error('Error enabling device:', err);
throw err; // Optionally, re-throw the error if you need to handle it upstream
}
}
async disconnect() {
try {
// Attempt to execute the disconnect command
await this.executeCmd(COMMANDS.CMD_EXIT, '');
} catch (err) {
// Log any errors encountered during command execution
console.error('Error during disconnection:', err);
// Optionally, add more handling or recovery logic here
}
// Attempt to close the socket and return the result
try {
return await this.closeSocket();
} catch (err) {
// Log any errors encountered while closing the socket
console.error('Error during socket closure:', err);
// Optionally, rethrow or handle the error if necessary
throw err; // Re-throwing to propagate the error
}
}
async getInfo() {
try {
// Execute the command to retrieve free sizes from the device
const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, '');
// Parse the response data to extract and return relevant information
return {
userCounts: data.readUIntLE(24, 4), // Number of users
logCounts: data.readUIntLE(40, 4), // Number of logs
logCapacity: data.readUIntLE(72, 4) // Capacity of logs in bytes
};
} catch (err) {
// Log the error for debugging purposes
console.error('Error getting device info:', err);
// Re-throw the error to allow upstream error handling
throw err;
}
}
async getVendor() {
const keyword = '~OEMVendor';
try {
// Execute the command to get serial number
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the serial number from the response data
const vendor = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return vendor;
} catch (err) {
// Log the error for debugging
console.error('Error getting vendor:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getProductTime() {
const keyword = '~ProductTime';
try {
// Execute the command to get serial number
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the serial number from the response data
const ProductTime = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return new Date(ProductTime);
} catch (err) {
// Log the error for debugging
console.error('Error getting Product Time:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getMacAddress() {
const keyword = 'MAC';
try {
// Execute the command to get serial number
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the serial number from the response data
const macAddr = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return macAddr;
} catch (err) {
// Log the error for debugging
console.error('Error getting MAC address:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getSerialNumber() {
const keyword = '~SerialNumber';
try {
// Execute the command to get serial number
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the serial number from the response data
const serialNumber = data.slice(8) // Skip the first 8 bytes (header)
.toString('utf-8') // Convert buffer to string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return serialNumber;
} catch (err) {
// Log the error for debugging
console.error('Error getting serial number:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getDeviceVersion() {
const keyword = '~ZKFPVersion';
try {
// Execute the command to get device version
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the device version from the response data
const deviceVersion = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return deviceVersion;
} catch (err) {
// Log the error for debugging
console.error('Error getting device version:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getDeviceName() {
const keyword = '~DeviceName';
try {
// Execute the command to get the device name
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the device name from the response data
const deviceName = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return deviceName;
} catch (err) {
// Log the error for debugging
console.error('Error getting device name:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getPlatform() {
const keyword = '~Platform';
try {
// Execute the command to get the platform information
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the platform information from the response data
const platform = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return platform;
} catch (err) {
// Log the error for debugging
console.error('Error getting platform information:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getOS() {
const keyword = '~OS';
try {
// Execute the command to get the OS information
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the OS information from the response data
const osInfo = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return osInfo;
} catch (err) {
// Log the error for debugging
console.error('Error getting OS information:', err);
// Re-throw the error for higher-level handling
throw err;
}
}
async getWorkCode() {
const keyword = 'WorkCode';
try {
// Execute the command to get the WorkCode information
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the WorkCode information from the response data
const workCode = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return workCode;
} catch (err) {
// Log the error for debugging
console.error('Error getting WorkCode:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async getPIN() {
const keyword = '~PIN2Width';
try {
// Execute the command to get the PIN information
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and format the PIN information from the response data
const pin = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, '') // Remove the keyword prefix
.replace(/\u0000/g, ''); // Remove null characters
return pin;
} catch (err) {
// Log the error for debugging
console.error('Error getting PIN:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async getFaceOn() {
const keyword = 'FaceFunOn';
try {
// Execute the command to get the face function status
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and process the status from the response data
const status = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, ''); // Remove the keyword prefix
// Determine and return the face function status
return status.includes('0') ? 'No' : 'Yes';
} catch (err) {
// Log the error for debugging
console.error('Error getting face function status:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async getSSR() {
const keyword = '~SSR';
try {
// Execute the command to get the SSR value
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword);
// Extract and process the SSR value from the response data
const ssrValue = data.slice(8) // Skip the first 8 bytes (header)
.toString('ascii') // Convert buffer to ASCII string
.replace(`${keyword}=`, ''); // Remove the keyword prefix
// Return the SSR value
return ssrValue;
} catch (err) {
// Log the error for debugging
console.error('Error getting SSR value:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async getFirmware() {
try {
// Execute the command to get firmware information
const data = await this.executeCmd(1100, '');
// Extract and return the firmware version from the response data
return data.slice(8).toString('ascii'); // Skip the first 8 bytes (header) and convert to ASCII string
} catch (err) {
// Log the error for debugging
console.error('Error getting firmware version:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async getTime() {
try {
// Execute the command to get the current time
const response = await this.executeCmd(COMMANDS.CMD_GET_TIME, '');
// Check if the response is valid
if (!response || response.length < 12) {
throw new Error('Invalid response received for time command');
}
// Extract and decode the time value from the response
const timeValue = response.readUInt32LE(8); // Read 4 bytes starting at offset 8
return timeParser.decode(timeValue); // Parse and return the decoded time
} catch (err) {
// Log the error for debugging
console.error('Error getting time:', err);
// Re-throw the error for the caller to handle
throw err;
}
}
async setTime(tm) {
try {
// Validate the input time
if (!(tm instanceof Date) && typeof tm !== 'number') {
throw new TypeError('Invalid time parameter. Must be a Date object or a timestamp.');
}
// Convert the input time to a Date object if it's not already
const date = (tm instanceof Date) ? tm : new Date(tm);
// Encode the time into the required format
const encodedTime = timeParser.encode(date);
// Create a buffer and write the encoded time
const commandString = Buffer.alloc(32);
commandString.writeUInt32LE(encodedTime, 0);
// Send the command to set the time
return await this.executeCmd(COMMANDS.CMD_SET_TIME, commandString);
} catch (err) {
// Log the error for debugging
console.error('Error setting time:', err);
// Re-throw the error for the caller to handle
throw err;
}
}
async voiceTest() {
try {
// Define the command data for the voice test
const commandData = Buffer.from('\x00\x00', 'binary');
// Execute the command and return the result
return await this.executeCmd(COMMANDS.CMD_TESTVOICE, commandData);
} catch (err) {
// Log the error for debugging purposes
console.error('Error executing voice test:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async setUser(uid, userid, name, password, role = 0, cardno = 0) {
try {
// Validate input parameters
if (
parseInt(uid) <= 0 || parseInt(uid) > 3000 ||
userid.length > 9 ||
name.length > 24 ||
password.length > 8 ||
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(parseInt(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(parseInt(cardno), 35);
commandBuffer.writeUInt32LE(0, 40); // Placeholder or reserved field
commandBuffer.write(userid.padEnd(9, '\0'), 48, 9); // Ensure userid is 9 bytes
// Send the command and return the result
return await this.executeCmd(COMMANDS.CMD_USER_WRQ, commandBuffer);
} catch (err) {
// Log error details for debugging
console.error('Error setting user:', err);
// Re-throw error for upstream handling
throw err;
}
}
async deleteUser(uid) {
try {
// Validate input parameter
if (parseInt(uid) <= 0 || parseInt(uid) > 3000) {
throw new Error('Invalid UID: must be between 1 and 3000');
}
// Allocate and initialize the buffer
const commandBuffer = Buffer.alloc(72);
// Write UID to the buffer
commandBuffer.writeUInt16LE(parseInt(uid), 0);
// Send the delete command and return the result
return await this.executeCmd(COMMANDS.CMD_DELETE_USER, commandBuffer);
} catch (err) {
// Log error details for debugging
console.error('Error deleting user:', err);
// Re-throw error for upstream handling
throw err;
}
}
async getAttendanceSize() {
try {
// Execute command to get free sizes
const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, '');
// Parse and return the attendance size
return data.readUIntLE(40, 4); // Assuming data at offset 40 represents the attendance size
} catch (err) {
// Log error details for debugging
console.error('Error getting attendance size:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
// Clears the attendance logs on the device
async clearAttendanceLog() {
try {
// Execute the command to clear attendance logs
return await this.executeCmd(COMMANDS.CMD_CLEAR_ATTLOG, '');
} catch (err) {
// Log the error for debugging purposes
console.error('Error clearing attendance log:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
// Clears all data on the device
async clearData() {
try {
// Execute the command to clear all data
return await this.executeCmd(COMMANDS.CMD_CLEAR_DATA, '');
} catch (err) {
// Log the error for debugging purposes
console.error('Error clearing data:', err);
// Re-throw the error to be handled by the caller
throw err;
}
}
async getRealTimeLogs(cb = () => {}) {
this.replyId++; // Increment the reply ID for this request
try {
// Create a buffer with the command header to request real-time logs
const buf = createTCPHeader(COMMANDS.CMD_REG_EVENT, this.sessionId, this.replyId, Buffer.from([0x01, 0x00, 0x00, 0x00]));
// Send the request to the device
this.socket.write(buf, null, (err) => {
if (err) {
// Log and reject the promise if there is an error writing to the socket
console.error('Error sending real-time logs request:', err);
throw err;
}
});
// Ensure data listeners are added only once
if (this.socket.listenerCount('data') === 0) {
this.socket.on('data', (data) => {
// Check if the data is an event and not just a regular response
if (checkNotEventTCP(data)) {
// Process the data if it is of the expected length
if (data.length > 16) {
// Decode and pass the log to the callback
cb(decodeRecordRealTimeLog52(data));
}
}
});
}
} catch (err) {
// Handle errors and reject the promise
console.error('Error getting real-time logs:', err);
throw err;
}
}
}
module.exports = ZTCP