UNPKG

node-zklib

Version:

light weight module for connect to biometrix device

479 lines (387 loc) 12.9 kB
const dgram = require('dgram') const { createUDPHeader, decodeUserData28, decodeRecordData16, decodeRecordRealTimeLog18, decodeUDPHeader, exportErrorMessage, checkNotEventUDP } = require('./utils') const { MAX_CHUNK, REQUEST_DATA, COMMANDS } = require('./constants') const { log } = require('./helpers/errorLog') class ZKLibUDP { 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, cbClose) { return new Promise((resolve, reject) => { this.socket = dgram.createSocket('udp4'); this.socket.setMaxListeners(Infinity) this.socket.once('error', err => { reject(err) cbError && cbError(err) }) this.socket.on('close', (err) => { this.socket = null; cbClose && cbClose('udp') }) this.socket.once('listening', () => { resolve(this.socket) }) try { this.socket.bind(this.inport) } catch (err) { } }) } 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('message') this.socket.close(() => { 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 sendTimeoutId; this.socket.once('message', (data) => { sendTimeoutId && clearTimeout(sendTimeoutId) resolve(data) }) this.socket.send(msg, 0, msg.length, this.port, this.ip, (err) => { if (err) { reject(err) } if (this.timeout) { sendTimeoutId = setTimeout(() => { clearTimeout(sendTimeoutId) reject(new Error('TIMEOUT_ON_WRITING_MESSAGE')) }, connect ? 2000 : this.timeout) } }) }) } requestData(msg) { return new Promise((resolve, reject) => { let sendTimeoutId const internalCallback = (data) => { sendTimeoutId && clearTimeout(sendTimeoutId) this.socket.removeListener('message', handleOnData) resolve(data) } const handleOnData = (data) => { if (checkNotEventUDP(data)) return; clearTimeout(sendTimeoutId) sendTimeoutId = setTimeout(() => { reject(new Error('TIMEOUT_ON_RECEIVING_REQUEST_DATA')) }, this.timeout) if (data.length >= 13) { internalCallback(data) } } this.socket.on('message', handleOnData) this.socket.send(msg, 0, msg.length, this.port, this.ip, (err) => { if (err) { reject(err) } sendTimeoutId = setTimeout(() => { reject(Error('TIMEOUT_IN_RECEIVING_RESPONSE_AFTER_REQUESTING_DATA')) }, this.timeout) }) }) } /** * * @param {*} command * @param {*} data * * * reject error when command fail and resolve data when success */ executeCmd(command, data) { return new Promise(async (resolve, reject) => { try { if (command === COMMANDS.CMD_CONNECT) { this.sessionId = 0 this.replyId = 0 } else { this.replyId++ } const buf = createUDPHeader(command, this.sessionId, this.replyId, data) const reply = await this.writeMessage(buf, command === COMMANDS.CMD_CONNECT || command === COMMANDS.CMD_EXIT) if (reply && reply.length && reply.length >= 0) { if (command === COMMANDS.CMD_CONNECT) { this.sessionId = reply.readUInt16LE(4); } } resolve(reply) } catch (err) { reject(err) } }) } 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) this.socket.send(buf, 0, buf.length, this.port, this.ip, (err) => { if (err) { if (err) { log(`[UDP][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 = createUDPHeader(COMMANDS.CMD_DATA_WRRQ, this.sessionId, this.replyId, reqData) let reply = null try { reply = await this.requestData(buf) } catch (err) { reject(err) } const header = decodeUDPHeader(reply.subarray(0, 8)) switch (header.commandId) { case COMMANDS.CMD_DATA: { resolve({ data: reply.subarray(8), mode: 8, err: null }) 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(8) 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 totalBuffer = Buffer.from([]) const timeout = 3000 let timer = setTimeout(() => { internalCallback(totalBuffer, new Error('TIMEOUT WHEN RECEIVING PACKET')) }, timeout) const internalCallback = (replyData, err = null) => { this.socket.removeListener('message', handleOnData) timer && clearTimeout(timer) if (err) { resolve({ err, data: replyData }) } else { resolve({ 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); 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 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 = 28 let userData = data.data.subarray(4) let users = [] 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 } } /** * * @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) } } if (data.mode) { // Data too small to decode in a normal way => we need a parameter to indicate this case const RECORD_PACKET_SIZE = 8 let recordData = data.data.subarray(4) let 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 } } else { const RECORD_PACKET_SIZE = 16 let recordData = data.data.subarray(4) let 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 } } } async freeData() { return await this.executeCmd(COMMANDS.CMD_FREE_DATA, '') } async getInfo() { const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, '') try { return { userCounts: data.readUIntLE(24, 4), logCounts: data.readUIntLE(40, 4), logCapacity: data.readUIntLE(72, 4) } } catch (err) { return Promise.reject(err) } } async clearAttendanceLog (){ return await this.executeCmd(COMMANDS.CMD_CLEAR_ATTLOG, '') } 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 getRealTimeLogs(cb = () => { }) { this.replyId++; const buf = createUDPHeader(COMMANDS.CMD_REG_EVENT, this.sessionId, this.replyId, REQUEST_DATA.GET_REAL_TIME_EVENT) this.socket.send(buf, 0, buf.length, this.port, this.ip, (err) => { }) this.socket.listenerCount('message') < 2 && this.socket.on('message', (data) => { if (!checkNotEventUDP(data)) return; if (data.length === 18) { cb(decodeRecordRealTimeLog18(data)) } }) } } module.exports = ZKLibUDP