zklib-js-zkteko
Version:
Improved JavaScript library for working with ZKTeco biometric attendance devices
688 lines (533 loc) • 17.8 kB
JavaScript
const net = require('net')
const { MAX_CHUNK, COMMANDS, REQUEST_DATA } = require('./constants')
const timeParser = require('./timestamp_parser');
const { createTCPHeader,
exportErrorMessage,
removeTcpHeader,
decodeUserData72,
decodeRecordRealTimeLog52,
checkNotEventTCP,
decodeTCPHeader,
decodeRecordData49} = require('./utils')
const { log } = require('./helpers/errorLog')
const { error } = require('console')
class ZKLibTCP {
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()
this.socket.once('error', err => {
reject(err)
cbError && cbError(err)
})
this.socket.once('connect', () => {
resolve(this.socket)
})
this.socket.once('close', (err) => {
this.socket = null;
cbClose && cbClose('tcp')
})
if (this.timeout) {
this.socket.setTimeout(this.timeout)
}
this.socket.connect(this.port, this.ip)
})
}
connect() {
return new Promise(async (resolve, reject) => {
try {
const reply = await this.executeCmd(COMMANDS.CMD_CONNECT, '')
if (reply) {
resolve(true)
} else {
reject(new Error('NO_REPLY_ON_CMD_CONNECT'))
}
} catch (err) {
reject(err)
}
})
}
closeSocket() {
return new Promise((resolve, reject) => {
this.socket.removeAllListeners('data')
this.socket.end(() => {
clearTimeout(timer)
resolve(true)
})
/**
* When socket isn't connected so this.socket.end will never resolve
* we use settimeout for handling this case
*/
const timer = setTimeout(() => {
resolve(true)
}, 2000)
})
}
writeMessage(msg, connect) {
return new Promise((resolve, reject) => {
let timer = null
this.socket.once('data', (data) => {
timer && clearTimeout(timer)
resolve(data)
})
this.socket.write(msg, null, async (err) => {
if (err) {
reject(err)
} else if (this.timeout) {
timer = await setTimeout(() => {
clearTimeout(timer)
reject(new Error('TIMEOUT_ON_WRITING_MESSAGE'))
}, connect ? 2000 : this.timeout)
}
})
})
}
requestData(msg) {
return new Promise((resolve, reject) => {
let timer = null
let replyBuffer = Buffer.from([])
const internalCallback = (data) => {
this.socket.removeListener('data', handleOnData)
timer && clearTimeout(timer)
resolve(data)
}
const handleOnData = (data) => {
replyBuffer = Buffer.concat([replyBuffer, data])
if (checkNotEventTCP(data)) return;
clearTimeout(timer)
const header = decodeTCPHeader(replyBuffer.subarray(0,16));
if(header.commandId === COMMANDS.CMD_DATA){
timer = setTimeout(()=>{
internalCallback(replyBuffer)
}, 1000)
}else{
timer = setTimeout(() => {
reject(new Error('TIMEOUT_ON_RECEIVING_REQUEST_DATA'))
}, this.timeout)
const packetLength = data.readUIntLE(4, 2)
if (packetLength > 8) {
internalCallback(data)
}
}
}
this.socket.on('data', handleOnData)
this.socket.write(msg, null, err => {
if (err) {
reject(err)
}
timer = setTimeout(() => {
reject(Error('TIMEOUT_IN_RECEIVING_RESPONSE_AFTER_REQUESTING_DATA'))
}, this.timeout)
})
}).catch(function () {
console.log("Promise Rejected");
});
}
/**
*
* @param {*} command
* @param {*} data
*
*
* reject error when command fail and resolve data when success
*/
executeCmd(command, data) {
return new Promise(async (resolve, reject) => {
if (command === COMMANDS.CMD_CONNECT) {
this.sessionId = 0
this.replyId = 0
} else {
this.replyId++
}
const buf = createTCPHeader(command, this.sessionId, this.replyId, data)
let reply = null
try{
reply = await this.writeMessage(buf, command === COMMANDS.CMD_CONNECT || command === COMMANDS.CMD_EXIT)
const rReply = removeTcpHeader(reply);
if (rReply && rReply.length && rReply.length >= 0) {
if (command === COMMANDS.CMD_CONNECT) {
this.sessionId = rReply.readUInt16LE(4);
}
}
resolve(rReply)
}catch(err){
reject(err)
}
})
}
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)
this.socket.write(buf, null, err => {
if (err) {
log(`[TCP][SEND_CHUNK_REQUEST]` + err.toString())
}
})
}
/**
*
* @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)))
}
}
})
}
async getSmallAttendanceLogs(){
}
/**
* reject error when starting request data
* return { data: users, err: Error } when receiving requested data
*/
async getUsers() {
// Free Buffer Data to request Data
if (this.socket) {
try {
await this.freeData()
} catch (err) {
return Promise.reject(err)
}
}
let data = null
try {
data = await this.readWithBuffer(REQUEST_DATA.GET_USERS)
} catch (err) {
return Promise.reject(err)
}
// Free Buffer Data after requesting data
if (this.socket) {
try {
await this.freeData()
} catch (err) {
return Promise.reject(err)
}
}
const USER_PACKET_SIZE = 72
let userData = data.data.subarray(4)
let users = []
while (userData.length >= USER_PACKET_SIZE) {
const user = decodeUserData72(userData.subarray(0, USER_PACKET_SIZE))
users.push(user)
userData = userData.subarray(USER_PACKET_SIZE)
}
return { data: users }
}
/**
*
* @param {*} ip
* @param {*} callbackInProcess
* reject error when starting request data
* return { data: records, err: Error } when receiving requested data
*/
async getAttendances(callbackInProcess = () => { }) {
if (this.socket) {
try {
await this.freeData()
} catch (err) {
return Promise.reject(err)
}
}
let data = null
try {
data = await this.readWithBuffer(REQUEST_DATA.GET_ATTENDANCE_LOGS, callbackInProcess)
} catch (err) {
return Promise.reject(err)
}
if (this.socket) {
try {
await this.freeData()
} catch (err) {
return Promise.reject(err)
}
}
const RECORD_PACKET_SIZE = 49
let recordData = data.data.subarray(4)
let records = []
while (recordData.length >= RECORD_PACKET_SIZE) {
const record = decodeRecordData49(recordData.subarray(0, RECORD_PACKET_SIZE))
records.push({ ...record, ip: this.ip })
recordData = recordData.subarray(RECORD_PACKET_SIZE)
}
return { data: records }
}
async freeData() {
return await this.executeCmd(COMMANDS.CMD_FREE_DATA, '')
}
async disableDevice() {
return await this.executeCmd(COMMANDS.CMD_DISABLEDEVICE, REQUEST_DATA.DISABLE_DEVICE)
}
async enableDevice() {
return await this.executeCmd(COMMANDS.CMD_ENABLEDEVICE, '')
}
async disconnect() {
try {
await this.executeCmd(COMMANDS.CMD_EXIT, '')
} catch (err) {
}
return await this.closeSocket()
}
async getInfo() {
try {
const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, '')
return {
userCounts: data.readUIntLE(24, 4),
logCounts: data.readUIntLE(40, 4),
logCapacity: data.readUIntLE(72, 4)
}
} catch (err) {
return Promise.reject(err)
}
}
async getSerialNumber() {
const keyword = '~SerialNumber';
try {
const data = await this.executeCmd(11, keyword)
return data.slice(8).toString('utf-8').replace(keyword + '=', '');
} catch (err) {
return Promise.reject(err)
}
}
async getDeviceVersion() {
const keyword = '~ZKFPVersion';;
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
return data.slice(8).toString('ascii').replace(keyword + '=', '');
} catch (err) {
return Promise.reject(err)
}
}
async getDeviceName() {
const keyword = '~DeviceName';
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
return data.slice(8).toString('ascii').replace(keyword + '=', '');
} catch (err) {
return Promise.reject(err)
}
}
async getPlatform() {
const keyword = '~Platform';
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
return data.slice(8).toString('ascii').replace(keyword + '=', '');
} catch (err) {
return Promise.reject(err)
}
}
async getOS() {
const keyword = '~OS';
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
return data.slice(8).toString('ascii').replace(keyword + '=', '');
} catch (err) {
return Promise.reject(err)
}
}
async getWorkCode() {
const keyword = 'WorkCode';
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
return data.slice(8).toString('ascii').replace(keyword + '=', '');
} catch (err) {
return Promise.reject(err)
}
}
async getPIN() {
const keyword = '~PIN2Width';
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
return data.slice(8).toString('ascii').replace(keyword + '=', '');
} catch (err) {
return Promise.reject(err)
}
}
async getFaceOn() {
const keyword = 'FaceFunOn';
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
if (data.slice(8).toString('ascii').replace(keyword + '=', '').includes('0'))
return 'No';
else
return 'Yes'
} catch (err) {
return Promise.reject(err)
}
}
async getSSR() {
const keyword = '~SSR';
try {
const data = await this.executeCmd(COMMANDS.CMD_OPTIONS_RRQ, keyword)
return data.slice(8).toString('ascii').replace(keyword + '=', '')
} catch (err) {
return Promise.reject(err)
}
}
async getFirmware() {
try {
const data = await this.executeCmd(1100, '')
return data.slice(8).toString('ascii')
} catch (err) {
return Promise.reject(err)
}
}
async getTime() {
try {
const t = await this.executeCmd(COMMANDS.CMD_GET_TIME, '')
return timeParser.decode(t.readUInt32LE(8));
}
catch(err){
return Promise.reject(err);
}
}
async setUser(uid, userid, name, password, role = 0, cardno = 0) {
try{
if (
parseInt(uid) === 0 ||
parseInt(uid) > 3000 ||
userid.length > 9 ||
name.length > 24 ||
password.length > 8 ||
cardno.length > 10
) {
return false;
}
const command_string = Buffer.alloc(72);
command_string.writeUInt16LE(uid, 0);
command_string.writeUInt16LE(role, 2);
command_string.write(password, 3, 8);
command_string.write(name, 11, 24);
command_string.writeUInt16LE(cardno, 35);
command_string.writeUInt32LE(0, 40);
command_string.write(userid ? userid.toString(9) : '', 48);
//console.log(command_string);
return await this.executeCmd(COMMANDS.CMD_USER_WRQ, command_string);
}
catch(e){
console.log(e);
console.log('duh');
}
}
async getAttendanceSize() {
try {
const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, '')
return data.readUIntLE(40, 4);
} catch (err) {
return Promise.reject(err)
}
}
async clearAttendanceLog (){
return await this.executeCmd(COMMANDS.CMD_CLEAR_ATTLOG, '')
}
async getRealTimeLogs(cb = () => { }) {
this.replyId++;
try{
const buf = createTCPHeader(COMMANDS.CMD_REG_EVENT, this.sessionId, this.replyId, Buffer.from([0x01, 0x00, 0x00, 0x00]))
this.socket.write(buf, null, err => {
})
this.socket.listenerCount('data') === 0 && this.socket.on('data', (data) => {
if (!checkNotEventTCP(data)) return;
if (data.length > 16) {
cb(decodeRecordRealTimeLog52(data))
}
})
}
catch(err){
return Promise.reject(err);
}
}
}
module.exports = ZKLibTCP