Last active
July 16, 2020 11:38
-
-
Save morkai/c82c820591d12ea8ed7d464827564d2e to your computer and use it in GitHub Desktop.
Balluff RFID processor controller
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const logger = require('h5.logger'); | |
const {BufferQueueReader} = require('h5.buffers'); | |
const TcpConnection = require('./TcpConnection'); | |
const MIN_ID_OCCURENCES = 3; | |
const STX = 0x02; | |
const EOT = 0x04; | |
const ACK = 0x06; | |
const NAK = 0x15; | |
class BalluffProcessorController | |
{ | |
constructor(options) | |
{ | |
this.options = options; | |
this.callback = null; | |
this.connection = null; | |
this.timers = {}; | |
this.stationI = -1; | |
this.reader = new BufferQueueReader(); | |
this.stations = options.stations.map(station => | |
{ | |
return { | |
no: station.stationNo, | |
current: { | |
id: '', | |
count: 0, | |
time: 0 | |
}, | |
candidate: { | |
id: '', | |
count: 0, | |
time: 0 | |
} | |
}; | |
}); | |
this.lastRead = []; | |
this.logger = logger.create({ | |
module: 'ct-balluff', | |
submodule: `${options.processorIp}:${options.headPort}` | |
}); | |
} | |
destroy() | |
{ | |
this.logger.info('Destroying...'); | |
this.callback = null; | |
Object.values(this.timers).forEach(timer => clearTimeout(timer)); | |
this.timers = {}; | |
if (this.connection) | |
{ | |
this.connection.close(); | |
this.connection = null; | |
} | |
this.reader.skip(this.reader.length); | |
} | |
start(callback) | |
{ | |
this.logger.info('Connecting...'); | |
this.callback = callback; | |
this.connection = new TcpConnection({ | |
socketOptions: { | |
host: this.options.processorIp, | |
port: this.options.headPort | |
}, | |
autoOpen: true, | |
autoReconnect: true, | |
minConnectTime: 2500, | |
maxReconnectTime: 5000, | |
noActivityTime: 5000, | |
closeOnDestroy: true, | |
suppressErrorsAfterDestroy: true | |
}); | |
this.connection.on('open', this.onOpen.bind(this)); | |
this.connection.on('close', this.onClose.bind(this)); | |
this.connection.on('error', this.onError.bind(this)); | |
this.connection.on('data', this.onData.bind(this)); | |
this.connection.on('write', this.onWrite.bind(this)); | |
} | |
onError(err) | |
{ | |
this.logger.error(err, 'Connection error.'); | |
} | |
onClose() | |
{ | |
this.logger.warn('Disconnected.'); | |
this.stationI = -1; | |
this.responseHandler = null; | |
} | |
onOpen() | |
{ | |
this.logger.warn('Connected.'); | |
this.stationI = -1; | |
this.responseHandler = null; | |
this.setKeepAlive(); | |
} | |
onData(data) | |
{ | |
if (0 && this.options.debug) | |
{ | |
this.printFrame('RX: ', data); | |
} | |
if (this.responseHandler) | |
{ | |
this.reader.push(data); | |
this.responseHandler.call(this); | |
} | |
} | |
onWrite(data) | |
{ | |
if (0 && this.options.debug) | |
{ | |
this.printFrame('TX: ', data); | |
} | |
} | |
setKeepAlive() | |
{ | |
this.request('%1500000', this.handleKeepAliveAck); | |
} | |
handleKeepAliveAck() | |
{ | |
if (this.reader.length === 2 && this.reader.readByte(0) === ACK && this.reader.readByte(1) === 0x30) | |
{ | |
this.request(Buffer.from([STX]), this.readNext); | |
} | |
else | |
{ | |
this.readNext(); | |
} | |
} | |
readNext() | |
{ | |
clearTimeout(this.timers.readNext); | |
const {stations} = this.options; | |
this.lastRead = []; | |
this.stationI += 1; | |
if (this.stationI === this.options.stations.length) | |
{ | |
this.stationI = 0; | |
} | |
const station = stations[this.stationI]; | |
if (this.options.debug) | |
{ | |
this.logger.debug('Reading code...', { | |
stationNo: station.stationNo, | |
headNo: station.headNo | |
}) | |
} | |
this.request(`M${station.headNo}T0030`, this.handleDataAck); | |
} | |
request(frame, responseHandler) | |
{ | |
if (!this.connection || !this.connection.isOpen()) | |
{ | |
return; | |
} | |
this.responseHandler = responseHandler; | |
this.reader.skip(this.reader.length); | |
this.connection.write(Buffer.isBuffer(frame) ? frame : this.frame(frame)); | |
} | |
frame(ascii) | |
{ | |
const buf = Buffer.from(ascii + '0'); | |
buf[buf.length - 1] = this.bcc(buf); | |
return buf; | |
} | |
bcc(buf) | |
{ | |
let bcc = 0; | |
if (buf instanceof BufferQueueReader) | |
{ | |
for (let i = 0; i < buf.length - 1; ++i) | |
{ | |
bcc = bcc ^ buf.readByte(i); | |
} | |
} | |
else | |
{ | |
for (let i = 0; i < buf.length - 1; ++i) | |
{ | |
bcc = bcc ^ buf[i]; | |
} | |
} | |
return bcc; | |
} | |
handleDataAck() | |
{ | |
const length = this.reader.length; | |
if (length < 1) | |
{ | |
return; | |
} | |
const status = this.reader.readByte(0); | |
if (length === 2 && status === NAK) | |
{ | |
if (this.options.debug) | |
{ | |
this.logger.debug('Read no codes.', { | |
stationNo: this.stations[this.stationI].no | |
}) | |
} | |
this.timers.readNext = setTimeout(this.readNext.bind(this), 100); | |
return; | |
} | |
if (length === 9 && status === ACK) | |
{ | |
this.request(Buffer.from([STX]), this.handleDataChunk); | |
} | |
} | |
handleDataChunk() | |
{ | |
const status = this.reader.readByte(0); | |
if ((status === ACK || status === EOT) && ((this.reader.length - 15) % 66 === 0)) | |
{ | |
const expectedChecksum = this.reader.readByte(this.reader.length - 1); | |
const actualChecksum = this.bcc(this.reader); | |
if (actualChecksum !== expectedChecksum) | |
{ | |
if (this.options.debug) | |
{ | |
this.logger.debug('Invalid checksum.', { | |
stationNo: this.stations[this.stationI].no, | |
expectedChecksum, | |
actualChecksum, | |
buffer: this.reader.shiftBuffer(this.reader.length) | |
}); | |
} | |
this.timers.readNext = setTimeout(this.readNext.bind(this), 100); | |
return; | |
} | |
this.reader.skip(14); | |
while (this.reader.length > 66) | |
{ | |
this.reader.skip(2); | |
const dataLength = this.reader.shiftByte(); | |
this.reader.skip(1); | |
const data = this.reader.shiftBuffer(62); | |
const id = []; | |
for (let i = 1; i <= dataLength; ++i) | |
{ | |
id.push(data[62 - i].toString(16).toUpperCase().padStart(2, '0')); | |
} | |
this.lastRead.push(id.join('')); | |
} | |
} | |
if (status === ACK) | |
{ | |
this.request(Buffer.from([STX]), this.handleDataChunk); | |
return; | |
} | |
this.handleData(); | |
this.readNext(); | |
} | |
handleData() | |
{ | |
const station = this.stations[this.stationI]; | |
if (!this.lastRead.length) | |
{ | |
if (this.options.debug) | |
{ | |
this.logger.debug('Read no codes.', { | |
stationNo: station.no | |
}); | |
} | |
return; | |
} | |
this.lastRead.sort(); | |
if (this.lastRead.includes(station.current.id)) | |
{ | |
if (this.options.debug) | |
{ | |
this.logger.debug('Read the same code.', { | |
stationNo: station.no, | |
lastRead: this.lastRead | |
}); | |
} | |
station.current.count += 1; | |
return; | |
} | |
if (this.lastRead.length > 1) | |
{ | |
if (this.options.debug) | |
{ | |
this.logger.debug('Read multiple codes.', { | |
stationNo: station.no, | |
lastRead: this.lastRead | |
}); | |
} | |
return; | |
} | |
if (this.lastRead[0] === station.candidate.id || this.lastRead.includes(station.candidate.id)) | |
{ | |
station.candidate.count += 1; | |
if (station.candidate.count === MIN_ID_OCCURENCES) | |
{ | |
station.current = {...station.candidate}; | |
if (this.options.debug) | |
{ | |
this.logger.debug('Read new code.', { | |
station, | |
lastRead: this.lastRead | |
}); | |
} | |
if (this.callback) | |
{ | |
this.callback(station.no, station.current); | |
} | |
} | |
else if (this.options.debug) | |
{ | |
this.logger.debug('Read matching candidate code.', { | |
stationNo: station.no, | |
lastRead: this.lastRead | |
}); | |
} | |
} | |
else if (this.lastRead.length === 1) | |
{ | |
station.candidate.id = this.lastRead[0]; | |
station.candidate.count = 1; | |
station.candidate.time = Date.now(); | |
if (this.options.debug) | |
{ | |
this.logger.debug('Read new candidate code.', { | |
station, | |
lastRead: this.lastRead | |
}); | |
} | |
} | |
else if (this.options.debug) | |
{ | |
this.logger.debug('Read ignored.', { | |
stationNo: station.no, | |
lastRead: this.lastRead | |
}); | |
} | |
} | |
printReader(prefix) | |
{ | |
this.printFrame(prefix, this.reader.readBuffer(0, this.reader.length)); | |
} | |
printFrame(prefix, frame) | |
{ | |
const ascii = []; | |
const hex = []; | |
const buffer = Buffer.isBuffer(frame); | |
for (let i = 0; i < frame.length; ++i) | |
{ | |
const b = buffer ? frame[i] : frame.readByte(i); | |
if (b >= 0x20 && b <= 0x7E) | |
{ | |
ascii.push(String.fromCharCode(b).padStart(2, ' ')); | |
} | |
else | |
{ | |
ascii.push(b.toString(16).padStart(2, '0')); | |
} | |
hex.push(b.toString(16).padStart(2, '0')); | |
} | |
if (prefix) | |
{ | |
console.log(prefix); | |
} | |
console.log(`${ascii.join(' ')}\n${hex.join(' ')}`); | |
} | |
} | |
module.exports = BalluffProcessorController; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
>>> warn 2020-07-14 19:33:34.425 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-14 19:33:34.427 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-14 19:57:21.699 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-14 19:57:21.699 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-14 19:57:32.466 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-14 19:57:32.468 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-14 20:04:46.166 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-14 20:04:46.166 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-14 20:04:57.249 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-14 20:04:57.251 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-14 20:14:41.410 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-14 20:14:41.410 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-14 20:14:52.860 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-14 20:14:52.862 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-14 20:21:53.654 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-14 20:21:53.655 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-14 20:22:05.410 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-14 20:22:05.412 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-14 20:47:47.987 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-14 20:47:47.988 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-14 21:17:37.863 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-14 21:17:37.866 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-14 22:47:50.604 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-14 22:47:50.643 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-14 22:47:50.645 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-14 23:17:39.286 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 00:47:52.050 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 00:47:52.053 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 00:47:53.478 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 01:17:42.153 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 02:47:54.876 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 02:47:54.878 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 02:47:54.899 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 03:17:43.545 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 04:47:56.308 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 04:47:56.310 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 04:47:57.715 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 04:47:57.718 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 06:47:59.132 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 06:48:00.545 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 07:17:44.663 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 07:17:48.065 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 08:47:55.808 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 08:47:57.384 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 09:17:44.973 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 09:17:46.409 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 10:47:57.717 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 10:47:59.157 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 11:17:47.308 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 11:17:48.042 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 12:48:00.066 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 12:48:00.778 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 13:17:49.804 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 13:17:49.811 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 14:47:57.575 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 14:47:57.578 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 14:47:57.599 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 14:47:57.600 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 16:48:00.444 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 16:48:00.446 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 16:48:00.455 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 16:48:00.458 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 18:48:03.297 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 18:48:03.298 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 19:17:53.756 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 19:17:53.757 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 20:48:01.527 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 20:48:01.527 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 20:48:01.530 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-15 21:17:50.192 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 22:48:02.923 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-15 22:48:02.926 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-15 22:48:04.328 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-15 22:48:04.330 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-16 00:48:02.143 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-16 00:48:02.145 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-16 00:48:05.728 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-16 00:48:05.730 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-16 02:48:03.545 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-16 02:48:03.548 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-16 02:48:04.942 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-16 02:48:04.944 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-16 04:48:06.340 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-16 04:48:07.744 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-16 04:48:07.746 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-16 05:17:54.999 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-16 06:48:07.741 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-16 06:48:10.539 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-16 06:48:10.542 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-16 07:17:56.378 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-16 08:48:08.382 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-16 08:48:08.385 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-16 08:48:09.142 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-16 09:17:57.686 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-16 10:48:10.417 [ct-balluff] [192.168.21.58:10001] Disconnected. | |
>>> warn 2020-07-16 10:48:11.234 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-16 10:48:12.744 [ct-balluff] [192.168.21.57:10001] Connected. | |
>>> warn 2020-07-16 10:48:12.938 [ct-balluff] [192.168.21.58:10001] Connected. | |
>>> warn 2020-07-16 10:48:25.248 [ct-balluff] [192.168.21.57:10001] Disconnected. | |
>>> warn 2020-07-16 10:48:25.441 [ct-balluff] [192.168.21.58:10001] Disconnected. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Part of <http://miracle.systems/p/h5.modbus> licensed under <MIT> | |
'use strict'; | |
const {EventEmitter} = require('events'); | |
const {Socket} = require('net'); | |
class TcpConnection extends EventEmitter | |
{ | |
/** | |
* @param {TcpConnectionOptions} [options] | |
*/ | |
constructor(options) | |
{ | |
super(); | |
/** | |
* @private | |
* @type {function(this:TcpConnection)} | |
*/ | |
this.onSocketConnect = this.onSocketConnect.bind(this); | |
/** | |
* @private | |
* @type {function(this:TcpConnection)} | |
*/ | |
this.onSocketClose = this.onSocketClose.bind(this); | |
/** | |
* @private | |
* @type {function(this:TcpConnection, Error)} | |
*/ | |
this.onSocketError = this.onSocketError.bind(this); | |
/** | |
* @private | |
* @type {function(this:TcpConnection)} | |
*/ | |
this.onSocketReadable = this.onSocketReadable.bind(this); | |
if (!options) | |
{ | |
options = {}; | |
} | |
/** | |
* @private | |
* @type {TcpSocketOptions} | |
*/ | |
this.socketOptions = Object.assign({port: 502, host: 'localhost'}, options.socketOptions); | |
/** | |
* @private | |
* @type {?Socket} | |
*/ | |
this.socket = this.setUpSocket(options.socket); | |
/** | |
* @private | |
* @type {boolean} | |
*/ | |
this.autoReconnect = options.autoReconnect !== false; | |
/** | |
* @private | |
* @type {number} | |
*/ | |
this.minConnectTime = options.minConnectTime || 2500; | |
/** | |
* @private | |
* @type {number} | |
*/ | |
this.maxReconnectTime = options.maxReconnectTime || 5000; | |
/** | |
* @private | |
* @type {number} | |
*/ | |
this.noActivityTime = options.noActivityTime || 0; | |
/** | |
* @private | |
* @type {boolean} | |
*/ | |
this.closeOnDestroy = options.closeOnDestroy !== false; | |
/** | |
* @private | |
* @type {boolean} | |
*/ | |
this.suppressErrorsAfterDestroy = options.suppressErrorsAfterDestroy !== false; | |
/** | |
* @private | |
* @type {boolean} | |
*/ | |
this.connected = this.socket.readyState === 'open'; | |
/** | |
* @private | |
* @type {boolean} | |
*/ | |
this.connecting = this.socket.readyState === 'opening'; | |
/** | |
* @private | |
* @type {boolean} | |
*/ | |
this.shouldReconnect = this.autoReconnect; | |
/** | |
* @private | |
* @type {number} | |
*/ | |
this.connectionAttempts = 0; | |
/** | |
* @private | |
* @type {number} | |
*/ | |
this.lastDataEventTime = 0; | |
/** | |
* @private | |
* @type {?number} | |
*/ | |
this.reconnectTimer = null; | |
/** | |
* @private | |
* @type {?number} | |
*/ | |
this.minConnectTimeTimer = null; | |
/** | |
* @private | |
* @type {?number} | |
*/ | |
this.noActivityTimer = null; | |
if (this.isOpen()) | |
{ | |
this.onSocketConnect(true); | |
} | |
else if (this.isOpening()) | |
{ | |
this.connectionAttempts += 1; | |
} | |
else if (options.autoOpen !== false) | |
{ | |
this.open(); | |
} | |
} | |
destroy() | |
{ | |
this.removeAllListeners(); | |
this.destroySocket(); | |
if (this.reconnectTimer !== null) | |
{ | |
clearTimeout(this.reconnectTimer); | |
this.reconnectTimer = null; | |
} | |
if (this.minConnectTimeTimer !== null) | |
{ | |
clearTimeout(this.minConnectTimeTimer); | |
this.minConnectTimeTimer = null; | |
} | |
if (this.noActivityTimer !== null) | |
{ | |
clearInterval(this.noActivityTimer); | |
this.noActivityTimer = null; | |
} | |
} | |
/** | |
* @returns {boolean} | |
*/ | |
isOpen() | |
{ | |
return this.connected; | |
} | |
/** | |
* @returns {boolean} | |
*/ | |
isOpening() | |
{ | |
return this.connecting; | |
} | |
open() | |
{ | |
if (this.isOpen() || this.isOpening()) | |
{ | |
return; | |
} | |
clearTimeout(this.reconnectTimer); | |
this.reconnectTimer = null; | |
this.connecting = true; | |
this.shouldReconnect = this.autoReconnect; | |
this.connectionAttempts += 1; | |
if (this.socket === null) | |
{ | |
this.socket = this.setUpSocket(null); | |
} | |
this.socket.connect(this.socketOptions); | |
} | |
close() | |
{ | |
this.doClose(false); | |
} | |
/** | |
* @param {Buffer} data | |
*/ | |
write(data) | |
{ | |
this.emit('write', data); | |
if (!this.isOpen()) | |
{ | |
return; | |
} | |
try | |
{ | |
this.socket.write(data); | |
} | |
catch (err) | |
{ | |
this.emit('error', err); | |
} | |
} | |
/** | |
* @private | |
* @param {Socket} [socket] | |
* @returns {Socket} | |
*/ | |
setUpSocket(socket) | |
{ | |
if (!socket) | |
{ | |
socket = new Socket(); | |
} | |
socket.on('connect', this.onSocketConnect); | |
socket.on('close', this.onSocketClose); | |
socket.on('error', this.onSocketError); | |
socket.on('readable', this.onSocketReadable); | |
socket.setNoDelay(this.socketOptions.noDelay !== false); | |
return socket; | |
} | |
/** | |
* @private | |
*/ | |
destroySocket() | |
{ | |
const socket = this.socket; | |
if (socket === null) | |
{ | |
return; | |
} | |
this.socket = null; | |
socket.removeListener('connect', this.onSocketConnect); | |
socket.removeListener('close', this.onSocketClose); | |
socket.removeListener('error', this.onSocketError); | |
socket.removeListener('readable', this.onSocketReadable); | |
if (this.suppressErrorsAfterDestroy) | |
{ | |
socket.on('error', () => {}); | |
} | |
if (this.closeOnDestroy) | |
{ | |
socket.destroy(); | |
} | |
} | |
/** | |
* @private | |
* @param {boolean} [doNotEmit] | |
*/ | |
onSocketConnect(doNotEmit) | |
{ | |
this.connecting = false; | |
this.connected = true; | |
clearTimeout(this.minConnectTimeTimer); | |
this.minConnectTimeTimer = setTimeout(this.onAfterMinConnectTime.bind(this), this.minConnectTime); | |
if (!doNotEmit) | |
{ | |
this.emit('open'); | |
} | |
this.onSocketReadable(); | |
} | |
/** | |
* @private | |
*/ | |
onSocketClose() | |
{ | |
if (this.minConnectTimeTimer !== null) | |
{ | |
clearTimeout(this.minConnectTimeTimer); | |
this.minConnectTimeTimer = null; | |
} | |
if (this.noActivityTimer !== null) | |
{ | |
clearInterval(this.noActivityTimer); | |
this.noActivityTimer = null; | |
} | |
this.connecting = false; | |
this.connected = false; | |
this.reconnect(); | |
this.emit('close'); | |
} | |
/** | |
* @private | |
* @param {Error} err | |
*/ | |
onSocketError(err) | |
{ | |
this.emit('error', err); | |
} | |
/** | |
* @private | |
*/ | |
onSocketReadable() | |
{ | |
while (true) // eslint-disable-line no-constant-condition | |
{ | |
const data = this.socket.read(); | |
if (data === null) | |
{ | |
this.lastDataEventTime = Date.now(); | |
break; | |
} | |
this.emit('data', data); | |
} | |
} | |
/** | |
* @private | |
*/ | |
onAfterMinConnectTime() | |
{ | |
this.connectionAttempts = 0; | |
this.minConnectTimeTimer = null; | |
this.setUpNoActivityTimer(); | |
} | |
/** | |
* @private | |
*/ | |
setUpNoActivityTimer() | |
{ | |
if (this.noActivityTime > 0 && this.noActivityTimer === null) | |
{ | |
this.noActivityTimer = setInterval( | |
this.checkActivity.bind(this), | |
this.noActivityTime | |
); | |
} | |
} | |
/** | |
* @private | |
*/ | |
checkActivity() | |
{ | |
const lastActivityTime = Date.now() - this.lastDataEventTime; | |
if (lastActivityTime > this.noActivityTime) | |
{ | |
this.connected = false; | |
this.doClose(true); | |
} | |
} | |
/** | |
* @private | |
*/ | |
reconnect() | |
{ | |
if (!this.shouldReconnect) | |
{ | |
return; | |
} | |
let reconnectTime = 250 * this.connectionAttempts; | |
if (reconnectTime > this.maxReconnectTime) | |
{ | |
reconnectTime = this.maxReconnectTime; | |
} | |
this.reconnectTimer = setTimeout(this.open.bind(this), reconnectTime); | |
} | |
/** | |
* @private | |
* @param {boolean} shouldReconnect | |
*/ | |
doClose(shouldReconnect) | |
{ | |
this.shouldReconnect = shouldReconnect; | |
if (this.reconnectTimer !== null) | |
{ | |
clearTimeout(this.reconnectTimer); | |
this.reconnectTimer = null; | |
} | |
if (this.socket !== null) | |
{ | |
this.socket.destroy(); | |
} | |
} | |
} | |
module.exports = TcpConnection; | |
/** | |
* @typedef {Object} TcpConnectionOptions | |
* @property {Socket} [socket] | |
* @property {TcpSocketOptions} [socketOptions={host: 'localhost', port: 502}] | |
* @property {boolean} [autoOpen=true] | |
* @property {boolean} [autoReconnect=true] | |
* @property {number} [minConnectTime=2500] | |
* @property {number} [maxReconnectTime=5000] | |
* @property {number} [noActivityTime=0] | |
* @property {boolean} [closeOnDestroy=true] | |
* @property {boolean} [suppressErrorsAfterDestroy=true] | |
*/ | |
/** | |
* @typedef {Object} TcpSocketOptions | |
* @property {string} [host=localhost] | |
* @property {number} [port=502] | |
* @property {string} [localAddress] | |
* @property {number} [localPort] | |
* @property {number} [family=4] | |
* @property {function(string, object, function(Error, string, number): void): void} [lookup] | |
* @property {string} [path] | |
* @property {boolean} [noDelay=true] | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment