From b071f45ae30facaa8bba2e2b7c3a5995dace9e9b Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 20:54:50 -0600 Subject: [PATCH 01/19] fixed spelling error --- src/client-request-handler.ts | 2 +- src/modbus-client.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/client-request-handler.ts b/src/client-request-handler.ts index a604b38..f632e2c 100644 --- a/src/client-request-handler.ts +++ b/src/client-request-handler.ts @@ -145,7 +145,7 @@ export default abstract class MBClientRequestHandler Date: Mon, 3 Feb 2020 20:56:16 -0600 Subject: [PATCH 02/19] added map utilities --- src/map-utils.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/map-utils.ts diff --git a/src/map-utils.ts b/src/map-utils.ts new file mode 100644 index 0000000..98b28e7 --- /dev/null +++ b/src/map-utils.ts @@ -0,0 +1,16 @@ +type FilterCallback = (value: T, index: number, array: T[]) => unknown +type MapCallback = (value: T, index: number, array: T[]) => U + +export class MapUtils { + public static ToArray (map: Map): Array<[K, V]> { + return Array.from(map) + } + + public static Filter (map: Map, cb: FilterCallback<[K, V]>): Map { + return new Map(this.ToArray(map).filter(cb)) + } + + public static Map (map: Map, cb: MapCallback<[K, V], U>) { + return new Map(this.ToArray(map).filter(cb)) + } +} From ae0db05dc60de1bfbadbf014bb02c3709bc0d4c1 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 20:56:44 -0600 Subject: [PATCH 03/19] created ModbusTCPClientManager --- .../modbus-tcp-client-manager.ts | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 src/client-manager/modbus-tcp-client-manager.ts diff --git a/src/client-manager/modbus-tcp-client-manager.ts b/src/client-manager/modbus-tcp-client-manager.ts new file mode 100644 index 0000000..11379e7 --- /dev/null +++ b/src/client-manager/modbus-tcp-client-manager.ts @@ -0,0 +1,216 @@ +import { Socket } from 'net' +import { MapUtils } from '../map-utils' +import ModbusTCPClient from '../modbus-tcp-client' + +// typealiases +type Host = string +type Port = number +type SlaveId = number +type SocketId = string +type ClientId = string + +interface ITCPInfo { + host: Host, + port: Port +} + +interface ITCPSlaveInfo extends ITCPInfo { + slaveId: SlaveId +} + +enum ErrorCode { + DUPLICATE_SOCKET = 'DUPLICATE_SOCKET', + DUPLICATE_CLIENT = 'DUPLICATE_SOCKET' +} + +export default class ModbusTCPClientManager { + + public static ErrorCode = ErrorCode + + public readonly clients = new Map() + public readonly sockets = new Map() + + public get socketCount () { + return this.sockets.size + } + + public get clientCount () { + return this.clients.size + } + + public findClient (tcpSlaveInfo: ITCPSlaveInfo) { + const clientId = this.marshalClientId(tcpSlaveInfo) + return this.clients.get(clientId) + } + + /** + * Returns a map of all clients that are bound to a specific socket + */ + public filterClientsBySocket (tcpInfo: ITCPInfo) { + return MapUtils.Filter(this.clients, + ( + ([clientId]) => { + const { host, port } = this.unmarshalClientId(clientId) + return tcpInfo.host === host && tcpInfo.port === port + } + ) + ) + } + + /** + * Finds or creates a modbus tcp client + */ + public findOrCreateClient (tcpClientInfo: ITCPSlaveInfo) { + return this.findClient(tcpClientInfo) || this.createClient(tcpClientInfo) + } + + /** + * Creates a modbus tcp client + */ + public createClient (tcpInfo: ITCPSlaveInfo, timeout = 2000) { + const socket = this.findOrCreateSocket(tcpInfo) + + if (this.findClient(tcpInfo) !== undefined) { + throw new Error(ErrorCode.DUPLICATE_CLIENT) + } + + const client = new ModbusTCPClient(socket, tcpInfo.slaveId, timeout) + const clientId = this.marshalClientId(tcpInfo) + this.clients.set(clientId, client) + return client + } + + /** + * Finds a modbus tcp client + */ + public findSocket (tcpInfo: ITCPInfo) { + return this.sockets.get(this.marshalSocketId(tcpInfo)) + } + + /** + * Finds or creates a tcp socket connection + * @param {string} host + * @param {number} port + */ + public findOrCreateSocket (tcpInfo: ITCPInfo) { + return this.findSocket(tcpInfo) || this.createSocket(tcpInfo) + } + + /** + * Creates a tcp socket connection + */ + public createSocket (tcpInfo: ITCPInfo) { + + if (this.findSocket(tcpInfo) !== undefined) { + throw new Error(ErrorCode.DUPLICATE_SOCKET) + } + + const socket = new Socket() + + socket.connect(tcpInfo) + + // set maximum listeners to the maximum number of clients for + // a single tcp master + socket.setMaxListeners(255) + + const socketId = this.marshalSocketId(tcpInfo) + this.sockets.set(socketId, socket) + + return socket + } + + /** + * Removes a tcp socket connection and all bound clients + */ + public removeSocket (tcpInfo: ITCPInfo) { + const socket = this.findSocket(tcpInfo) + if (socket) { + this.removeClientsBySocket(tcpInfo) + socket.end() + const socketId = this.marshalSocketId(tcpInfo) + this.sockets.delete(socketId) + } + } + + /** + * Removes a modbus tcp client + */ + public removeClient (tcpSlaveInfo: ITCPSlaveInfo) { + const client = this.findClient(tcpSlaveInfo) + if (client) { + client.manuallyRejectAllRequests() + this.clients.delete(this.marshalClientId(tcpSlaveInfo)) + } + } + + /** + * Removes all clients assosciated to a single socket + */ + public removeClientsBySocket (tcpInfo: ITCPInfo) { + const clients = this.filterClientsBySocket(tcpInfo) + for (const [clientId] of clients) { + const { slaveId } = this.unmarshalClientId(clientId) + this.removeClient({ + host: tcpInfo.host, + port: tcpInfo.port, + slaveId + }) + } + } + + /** + * Removes all unused sockets. An unused socket is a socket + * that has no clients that use it + */ + public removeAllUnusedSockets () { + const socketsWithoutClients = this.findSocketsWithoutClients() + for (const [socketId] of socketsWithoutClients) { + const { host, port } = this.unmarshalSocketId(socketId) + this.removeSocket({ host, port }) + } + } + + /** + * Finds sockets that do not have any clients using it + */ + private findSocketsWithoutClients () { + const unusedSocketMap = new Map() + + for (const [socketId, socket] of this.sockets) { + const { host, port } = this.unmarshalSocketId(socketId) + const clients = this.filterClientsBySocket({ host, port }) + if (clients.size === 0) { + unusedSocketMap.set(socketId, socket) + } + } + + return unusedSocketMap + } + + private marshalSocketId ({ host, port }: ITCPInfo): SocketId { + return `${host}:${port}` + } + + private unmarshalSocketId (socketId: SocketId): ITCPInfo { + const [host, port] = socketId.split(':') + return { + host, + port: parseInt(port, 10) + } + } + + private marshalClientId ({ host, port, slaveId }: ITCPSlaveInfo): ClientId { + return `${host}:${port}.${slaveId}` + } + + private unmarshalClientId (clientId: ClientId): ITCPSlaveInfo { + const [host, remainder] = clientId.split(':') + const [port, slaveId] = remainder.split('.') + + return { + host, + port: parseInt(port, 10), + slaveId: parseInt(slaveId, 10) + } + } +} From 08ce0c6c297925015dc4424a6a95bf04b2a1f88c Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 20:56:51 -0600 Subject: [PATCH 04/19] created ModbusRTUClientManager --- .../modbus-rtu-client-manager.ts | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 src/client-manager/modbus-rtu-client-manager.ts diff --git a/src/client-manager/modbus-rtu-client-manager.ts b/src/client-manager/modbus-rtu-client-manager.ts new file mode 100644 index 0000000..b82811f --- /dev/null +++ b/src/client-manager/modbus-rtu-client-manager.ts @@ -0,0 +1,195 @@ +import SerialPort, { OpenOptions } from 'serialport' +import { MapUtils } from '../map-utils' +import ModbusRTUClient from '../modbus-rtu-client' + +// typealiases +type SlaveId = number +type SocketId = string +type ClientId = string + +interface IRTUInfo extends OpenOptions { + path: string +} + +interface IRTUSlaveInfo extends IRTUInfo { + slaveId: SlaveId +} + +export default class ModbusRTUClientManager { + + public static async ListSerialPorts () { + const ports = await SerialPort.list() + + return ports + } + + public readonly clients = new Map() + public readonly sockets = new Map() + + public get socketCount () { + return this.sockets.size + } + + public get clientCount () { + return this.clients.size + } + + public findClient (rtuSlaveInfo: IRTUSlaveInfo) { + const clientId = this.marshalClientId(rtuSlaveInfo) + return this.clients.get(clientId) + } + + /** + * Returns a map of all clients that are bound to a specific socket + */ + public filterClientsBySocket (rtuInfo: IRTUInfo) { + return MapUtils.Filter(this.clients, + ( + ([clientId]) => { + const { path } = this.unmarshalClientId(clientId) + return rtuInfo.path === path + } + ) + ) + } + + /** + * Finds or creates a modbus rtu client + */ + public findOrCreateClient (rtuClientInfo: IRTUSlaveInfo) { + return this.findClient(rtuClientInfo) || this.createClient(rtuClientInfo) + } + + /** + * Creates a modbus rtu client + */ + public createClient (rtuInfo: IRTUSlaveInfo, timeout = 2000) { + const socket = this.findOrCreateSocket(rtuInfo) + const client = new ModbusRTUClient(socket, rtuInfo.slaveId, timeout) + const clientId = this.marshalClientId(rtuInfo) + this.clients.set(clientId, client) + return client + } + + /** + * Finds a modbus rtu client + */ + public findSocket (rtuInfo: IRTUInfo) { + return this.sockets.get(this.marshalSocketId(rtuInfo)) + } + + /** + * Finds or creates a rtu socket connection + * @param {string} host + * @param {number} port + */ + public findOrCreateSocket (rtuInfo: IRTUInfo) { + return this.findSocket(rtuInfo) || this.createSocket(rtuInfo) + } + + /** + * Creates a rtu socket connection + */ + public createSocket (rtuInfo: IRTUInfo) { + const { + path, + ...options + } = rtuInfo + const socket = new SerialPort(path, options) + + const socketId = this.marshalSocketId(rtuInfo) + this.sockets.set(socketId, socket) + + return socket + } + + /** + * Removes a rtu socket connection and all bound clients + */ + public removeSocket (rtuInfo: IRTUInfo) { + const socket = this.findSocket(rtuInfo) + if (socket) { + this.removeClientsBySocket(rtuInfo) + socket.end() + const socketId = this.marshalSocketId(rtuInfo) + this.sockets.delete(socketId) + } + } + + /** + * Removes a modbus rtu client + */ + public removeClient (rtuSlaveInfo: IRTUSlaveInfo) { + const client = this.findClient(rtuSlaveInfo) + if (client) { + client.manuallyRejectAllRequests() + this.clients.delete(this.marshalClientId(rtuSlaveInfo)) + } + } + + /** + * Removes all clients assosciated to a single socket + */ + public removeClientsBySocket (rtuInfo: IRTUInfo) { + const clients = this.filterClientsBySocket(rtuInfo) + for (const [clientId] of clients) { + const { slaveId } = this.unmarshalClientId(clientId) + this.removeClient({ + path: rtuInfo.path, + slaveId + }) + } + } + + /** + * Removes all unused sockets. An unused socket is a socket + * that has no clients that use it + */ + public removeAllUnusedSockets () { + const socketsWithoutClients = this.findSocketsWithoutClients() + for (const [socketId] of socketsWithoutClients) { + const rtuInfo = this.unmarshalSocketId(socketId) + this.removeSocket(rtuInfo) + } + } + + /** + * Finds sockets that do not have any clients using it + */ + private findSocketsWithoutClients () { + const unusedSocketMap = new Map() + + for (const [socketId, socket] of this.sockets) { + const rtuInfo = this.unmarshalSocketId(socketId) + const clients = this.filterClientsBySocket(rtuInfo) + if (clients.size === 0) { + unusedSocketMap.set(socketId, socket) + } + } + + return unusedSocketMap + } + + private marshalSocketId ({ path }: IRTUInfo): SocketId { + return `${path}` + } + + private unmarshalSocketId (socketId: SocketId): IRTUInfo { + return { + path: socketId + } + } + + private marshalClientId ({ path, slaveId }: IRTUSlaveInfo): ClientId { + return `${path}.${slaveId}` + } + + private unmarshalClientId (clientId: ClientId): IRTUSlaveInfo { + const [path, slaveId] = clientId.split('.') + + return { + path, + slaveId: parseInt(slaveId, 10) + } + } +} From 08f6505987b3178eef502a92edf1162016e34281 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 20:59:10 -0600 Subject: [PATCH 05/19] SerialPort import should be default needs to be default for extending the rtu modbus client --- src/modbus-rtu-client.ts | 2 +- src/rtu-client-request-handler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modbus-rtu-client.ts b/src/modbus-rtu-client.ts index 07cadf5..166e294 100644 --- a/src/modbus-rtu-client.ts +++ b/src/modbus-rtu-client.ts @@ -3,7 +3,7 @@ import MBClient from './modbus-client.js' import ModbusRTUClientRequestHandler from './rtu-client-request-handler.js' import ModbusRTUClientResponseHandler from './rtu-client-response-handler.js' -import * as SerialPort from 'serialport' +import SerialPort from 'serialport' import ModbusRTURequest from './rtu-request.js' import ModbusRTUResponse from './rtu-response.js' diff --git a/src/rtu-client-request-handler.ts b/src/rtu-client-request-handler.ts index 177a9b6..fbedbcd 100644 --- a/src/rtu-client-request-handler.ts +++ b/src/rtu-client-request-handler.ts @@ -1,7 +1,7 @@ import Debug = require('debug'); const debug = Debug('rtu-client-request-handler') import CRC from 'crc' -import * as SerialSocket from 'serialport' +import SerialSocket from 'serialport' import MBClientRequestHandler from './client-request-handler.js' import ModbusRequestBody from './request/request-body.js' import ModbusRTURequest from './rtu-request.js' From 79cca170dc41d5b8cfd4781f19503cb219d54bd0 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 20:59:54 -0600 Subject: [PATCH 06/19] export modbus client manager at root file --- src/client-manager/index.ts | 2 ++ src/modbus.ts | 1 + 2 files changed, 3 insertions(+) create mode 100644 src/client-manager/index.ts diff --git a/src/client-manager/index.ts b/src/client-manager/index.ts new file mode 100644 index 0000000..a70c7f2 --- /dev/null +++ b/src/client-manager/index.ts @@ -0,0 +1,2 @@ +export { default as ModbusRTUClientManager } from './modbus-rtu-client-manager' +export { default as ModbusTCPClientManager } from './modbus-tcp-client-manager' diff --git a/src/modbus.ts b/src/modbus.ts index 06cde1a..c7a1fe2 100644 --- a/src/modbus.ts +++ b/src/modbus.ts @@ -70,6 +70,7 @@ export { default as MBClientRequestHandler } from './client-request-handler' export { default as ModbusClientResponseHandler } from './client-response-handler' export { default as ModbusClient } from './modbus-client' export * from './request-response-map' +export * from './client-manager' export { default as ModbusTCPRequest } from './tcp-request' export { default as ModbusTCPResponse } from './tcp-response' export { UserRequestError } from './user-request-error' From 0e78063b2ed75c0e61c6b485997780f901717a93 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 21:01:13 -0600 Subject: [PATCH 07/19] ModbusTCPClientManager tests init commit --- test/tcp-client-manager.test.js | 140 ++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 test/tcp-client-manager.test.js diff --git a/test/tcp-client-manager.test.js b/test/tcp-client-manager.test.js new file mode 100644 index 0000000..05db4c8 --- /dev/null +++ b/test/tcp-client-manager.test.js @@ -0,0 +1,140 @@ +'use strict' + +/* global describe, it, beforeEach */ + +const assert = require('assert') +const { + ModbusTCPClientManager, + ModbusTCPClient, +} = require('../dist/modbus') +const sinon = require('sinon') +const EventEmitter = require('events') + +describe('ModbusTCPClientManager Tests.', () => { + /** + * @type {ModbusTCPClientManager} + */ + let manager + + beforeEach(function () { + manager = new ModbusTCPClientManager(); + }) + + function addClient(host = 'localhost', port = 5052, slaveId = 1) { + + const client = manager.findOrCreateClient({ + host, + port, + slaveId, + }) + + assert.equal(client instanceof ModbusTCPClient, true) + return client + } + + function addMultipleSlavesById(host = 'localhost', port = 5052, slaveCount = 5, slaveStart = 1){ + for(let slaveId = slaveStart; slaveId < (slaveCount + slaveStart); i++){ + addClient(host, port, slaveId) + } + } + + function checkEmptyManager() { + assert.equal(manager.clientCount, 0, 'Number of clients should be zero') + assert.equal(manager.socketCount, 0, 'Number of sockets should be zero') + } + + + it('should add a client', () => { + checkEmptyManager() + + const host = 'localhost', port = 5052, slaveId = 1 + + manager.createClient({ + host, + port, + slaveId, + }) + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should find a created client', () => { + checkEmptyManager() + + const host = 'localhost', port = 5052, slaveId = 1 + + const client = manager.createClient({ + host, + port, + slaveId, + }) + + const foundClient = manager.findClient({ + host, + port, + slaveId, + }) + + assert.notEqual(foundClient, undefined, 'Searched for client should be defined') + assert.equal(client, foundClient, 'Should be the same client') + assert.equal(client.slaveId, foundClient.slaveId, 'Should have the same slaveId') + assert.equal(client.socket.localAddress, foundClient.socket.localAddress, 'Should have the same localAddress') + assert.equal(client.socket.localPort, foundClient.socket.localPort, 'Should have the same localAddress') + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should get accurate socketCount & clientCount', () => { + addMultipleSlavesById('localhost', 5052, 10) + addMultipleSlavesById('localhost', 5052, 15) + + assert.equal(manager.clientCount, 25, 'Number of clients should be 25') + assert.equal(manager.socketCount, 2, 'Number of sockets should be 2') + }) + + it('should create a client where the socket only has a maximum of 255 listeners', () => { + checkEmptyManager() + + const client = addClient() + + assert.equal(client.socket.getMaxListeners(), 255, 'Number of listeners should equal 255') + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should only add one socket for clients with the same host & port', () => { + checkEmptyManager() + + const host = 'localhost' + const port = 5052 + + const numClients = 255; + + for (let slaveId = 1; slaveId < numClients; slaveId++){ + addClient(host, port, slaveId) + } + + assert.equal(manager.clientCount, numClients - 1, `Number of clients should be ${numClients}`) + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should not allow duplicate clients to be created', () => { + checkEmptyManager() + + const host = 'localhost' + const port = 5052 + const slaveId = 1; + + manager.createClient({ host, port, slaveId }) + + assert.throws(() => manager.createClient({ host, port, slaveId })) + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + +}) From 12d5b6b365f18023a487f4f1279db222cf93cd6a Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 21:18:22 -0600 Subject: [PATCH 08/19] removed unused map utils and added tests --- src/map-utils.ts | 4 --- test/map-utils.test.js | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 test/map-utils.test.js diff --git a/src/map-utils.ts b/src/map-utils.ts index 98b28e7..67ca902 100644 --- a/src/map-utils.ts +++ b/src/map-utils.ts @@ -9,8 +9,4 @@ export class MapUtils { public static Filter (map: Map, cb: FilterCallback<[K, V]>): Map { return new Map(this.ToArray(map).filter(cb)) } - - public static Map (map: Map, cb: MapCallback<[K, V], U>) { - return new Map(this.ToArray(map).filter(cb)) - } } diff --git a/test/map-utils.test.js b/test/map-utils.test.js new file mode 100644 index 0000000..d3bd5f6 --- /dev/null +++ b/test/map-utils.test.js @@ -0,0 +1,55 @@ +'use strict' + +/* global describe, it, beforeEach */ + +const assert = require('assert') +const { + MapUtils, +} = require('../dist/map-utils') + +describe('MapUtils Tests.', () => { + + it('should convert a map to an array', () => { + + const testMap = new Map([ + [0, 'A'], + [1, 'B'], + [2, 'C'], + [4, 'D'], + ]) + + const arrayResponse = MapUtils.ToArray(testMap) + + assert.equal(arrayResponse instanceof Array, true, 'ToArray response should be an array') + + for(const [key, value] of arrayResponse){ + assert.equal(typeof key, 'number', 'Key should be a number') + assert.equal(typeof value, 'string', 'Value should be a string') + } + + }) + + it('should filter the map by a criteria and return a new map', () => { + + const testData = [ + [0, 'A'], + [1, 'B'], + [2, 'C'], + [4, 'D'], + ]; + + const testMap = new Map(testData) + + const filteredMap = MapUtils.Filter(testMap, ([key]) => key > 1) + + assert.equal(filteredMap instanceof Map, true, 'Filter response should be a Map') + + assert.equal(filteredMap.size, 2) + + for(const [key, value] of filteredMap){ + assert.equal(typeof key, 'number', 'Key should be a number') + assert.equal(typeof value, 'string', 'Value should be a string') + } + + }) +}) \ No newline at end of file From 9d43e2cc2043a23410dc6e32892946153b9e34ee Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 21:41:57 -0600 Subject: [PATCH 09/19] added additional ModbusTCPClientManager tests --- test/tcp-client-manager.test.js | 173 +++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 4 deletions(-) diff --git a/test/tcp-client-manager.test.js b/test/tcp-client-manager.test.js index 05db4c8..76f3d6c 100644 --- a/test/tcp-client-manager.test.js +++ b/test/tcp-client-manager.test.js @@ -7,8 +7,7 @@ const { ModbusTCPClientManager, ModbusTCPClient, } = require('../dist/modbus') -const sinon = require('sinon') -const EventEmitter = require('events') +const { Socket } = require('net') describe('ModbusTCPClientManager Tests.', () => { /** @@ -33,7 +32,7 @@ describe('ModbusTCPClientManager Tests.', () => { } function addMultipleSlavesById(host = 'localhost', port = 5052, slaveCount = 5, slaveStart = 1){ - for(let slaveId = slaveStart; slaveId < (slaveCount + slaveStart); i++){ + for(let slaveId = slaveStart; slaveId < (slaveCount + slaveStart); slaveId++){ addClient(host, port, slaveId) } } @@ -88,12 +87,178 @@ describe('ModbusTCPClientManager Tests.', () => { it('should get accurate socketCount & clientCount', () => { addMultipleSlavesById('localhost', 5052, 10) - addMultipleSlavesById('localhost', 5052, 15) + addMultipleSlavesById('localhost', 5053, 15) assert.equal(manager.clientCount, 25, 'Number of clients should be 25') assert.equal(manager.socketCount, 2, 'Number of sockets should be 2') }) + it('should filterClientsBySocket', () => { + + const testHost = 'localhost' + const testPort = 5055 + const testNumberOfClients = 15 + + addMultipleSlavesById('localhost', 5052, 10) + addMultipleSlavesById('localhost', 5053, 5) + addMultipleSlavesById('localhost', 5054, 2) + addMultipleSlavesById(testHost, testPort, testNumberOfClients) + + const clients = manager.filterClientsBySocket({host: testHost, port: testPort}) + + assert.equal(clients.size, testNumberOfClients, 'The number of clients should match the socket it is assigned to') + + const socket = manager.findSocket({ + host: testHost, + port: testPort + }) + for(const [_, client] of clients){ + assert.equal(socket, client.socket, 'Client socket should be the same socket for all clients') + } + }) + + it('should findOrCreateClient but not duplicate', () => { + const testHost = 'localhost' + const testPort = 5055 + const testSlaveId = 6 + + const client_1 = manager.findOrCreateClient({ + host: testHost, + port: testPort, + slaveId: testSlaveId + }) + + const client_2 = manager.findOrCreateClient({ + host: testHost, + port: testPort, + slaveId: testSlaveId + }) + + assert.equal(client_1 instanceof ModbusTCPClient, true, 'Client 1 should be an instance of ModbusTCPClient') + assert.equal(client_2 instanceof ModbusTCPClient, true, 'Client 2 should be an instance of ModbusTCPClient') + assert.equal(client_1, client_2, 'Client 1 should be the same reference as Client 2') + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should findSocket', () => { + const testHost = 'localhost' + const testPort = 5055 + + addClient(testHost, testPort) + + const socket = manager.findSocket({ + host: testHost, + port: testPort + }) + + assert.equal(socket instanceof Socket, true, 'Socket should be an instance of Socket') + }) + + it('should findOrCreateSocket', () => { + const testHost = 'localhost' + const testPort = 5055 + + const socket = manager.findOrCreateSocket({ + host: testHost, + port: testPort + }) + + assert.equal(socket instanceof Socket, true, 'Socket should be an instance of Socket') + }) + + it('should removeSocket and all clients bound to that socket', () => { + const testHost = 'localhost' + const testPort = 5055 + const testNumberOfClients = 15 + + addMultipleSlavesById('localhost', 5052, 10) + addMultipleSlavesById('localhost', 5053, 5) + addMultipleSlavesById('localhost', 5054, 2) + addMultipleSlavesById(testHost, testPort, testNumberOfClients) + + let clients = manager.filterClientsBySocket({host: testHost, port: testPort}) + + assert.equal(clients.size, testNumberOfClients, 'The number of clients should match the socket it is assigned to') + + manager.removeSocket({ + host: testHost, + port: testPort + }) + + clients = manager.filterClientsBySocket({host: testHost, port: testPort}) + + assert.equal(clients.size, 0, 'The number of clients after removing the socket should be zero') + + }) + + it('should removeSocket and manually reject all requests for the clients', () => { + // TODO + }) + + it('should removeClient and manually reject all of the clients requests', () => { + // TODO + }) + + it('should remove clients by socket but not remove the original socket', () => { + const testHost = 'localhost' + const testPort = 5055 + const testNumberOfClients = 15 + + addMultipleSlavesById('localhost', 5052, 10) + addMultipleSlavesById('localhost', 5053, 5) + addMultipleSlavesById('localhost', 5054, 2) + addMultipleSlavesById(testHost, testPort, testNumberOfClients) + + let clients = manager.filterClientsBySocket({host: testHost, port: testPort}) + + assert.equal(clients.size, testNumberOfClients, 'The number of clients should match the socket it is assigned to') + + manager.removeClientsBySocket({ + host: testHost, + port: testPort + }) + + clients = manager.filterClientsBySocket({host: testHost, port: testPort}) + + assert.equal(clients.size, 0, 'The number of clients should be zero') + + const socket = manager.findSocket({ + host: testHost, + port: testPort + }) + + assert.equal(socket instanceof Socket, true, 'Socket should be an instance of Socket') + }) + + it('should remove all unused sockets', () => { + const testHost = 'localhost' + const testPort = 5055 + + addMultipleSlavesById('localhost', 5052, 10) + addMultipleSlavesById('localhost', 5053, 5) + addMultipleSlavesById('localhost', 5054, 2) + addMultipleSlavesById(testHost, testPort, 0) + + let clients = manager.filterClientsBySocket({host: testHost, port: testPort}) + + assert.equal(clients.size, 0, 'The number of clients should be zero') + + manager.removeAllUnusedSockets() + + const socket = manager.findSocket({ + host: testHost, + port: testPort + }) + + assert.equal(socket, undefined, 'Socket not be found') + }) + + it('should findSocketsWithoutClients', () => { + // TODO + }) + it('should create a client where the socket only has a maximum of 255 listeners', () => { checkEmptyManager() From 9f59c420d586ffc16d615c9337163f5cae3d3f83 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 22:11:43 -0600 Subject: [PATCH 10/19] fixed bugs in ModbusTCPClientManager tests --- test/tcp-client-manager.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/tcp-client-manager.test.js b/test/tcp-client-manager.test.js index 76f3d6c..58dee3e 100644 --- a/test/tcp-client-manager.test.js +++ b/test/tcp-client-manager.test.js @@ -239,7 +239,11 @@ describe('ModbusTCPClientManager Tests.', () => { addMultipleSlavesById('localhost', 5052, 10) addMultipleSlavesById('localhost', 5053, 5) addMultipleSlavesById('localhost', 5054, 2) - addMultipleSlavesById(testHost, testPort, 0) + + manager.createSocket({ + host: testHost, + port: testPort + }) let clients = manager.filterClientsBySocket({host: testHost, port: testPort}) @@ -255,10 +259,6 @@ describe('ModbusTCPClientManager Tests.', () => { assert.equal(socket, undefined, 'Socket not be found') }) - it('should findSocketsWithoutClients', () => { - // TODO - }) - it('should create a client where the socket only has a maximum of 255 listeners', () => { checkEmptyManager() From 3a941f78b2b5ee16f97183e3c14442e09d009eae Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 22:12:05 -0600 Subject: [PATCH 11/19] added ModbusRTUClientManager tests --- test/rtu-client-manager.test.js | 308 ++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 test/rtu-client-manager.test.js diff --git a/test/rtu-client-manager.test.js b/test/rtu-client-manager.test.js new file mode 100644 index 0000000..01b2ddb --- /dev/null +++ b/test/rtu-client-manager.test.js @@ -0,0 +1,308 @@ +'use strict' + +/* global describe, it, beforeEach */ + +const assert = require('assert') +const { + ModbusRTUClientManager, + ModbusRTUClient, +} = require('../dist/modbus') +const SerialPort = require('serialport') + +describe('ModbusRTUClientManager Tests.', () => { + /** + * @type {ModbusRTUClientManager} + */ + let manager + /** + * @type {SerialPort.OpenOptions} + */ + let defaultSerialPortOptions + + const defaultSerialPortPath = '///this///is///not///a///real///path' + + beforeEach(function () { + manager = new ModbusRTUClientManager(); + defaultSerialPortOptions = { + baudRate: 9600, + dataBits: 8, + autoOpen: false + } + }) + + function generateRandomPath() { + return defaultSerialPortPath + Math.floor(Math.random() * 100) + } + + function addClient(path = defaultSerialPortPath, slaveId = 1, options = defaultSerialPortOptions) { + + const client = manager.findOrCreateClient({ + path, + slaveId, + ...options + }) + + assert.equal(client instanceof ModbusRTUClient, true) + return client + } + + function addMultipleSlavesById(host = defaultSerialPortPath, slaveCount = 5, slaveStart = 1){ + for(let slaveId = slaveStart; slaveId < (slaveCount + slaveStart); slaveId++){ + addClient(host, slaveId) + } + } + + function checkEmptyManager() { + assert.equal(manager.clientCount, 0, 'Number of clients should be zero') + assert.equal(manager.socketCount, 0, 'Number of sockets should be zero') + } + + + it('should add a client', () => { + checkEmptyManager() + + const path = defaultSerialPortPath, slaveId = 1 + + manager.createClient({ + path, + slaveId, + }) + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should find a created client', () => { + checkEmptyManager() + + const path = defaultSerialPortPath, slaveId = 1 + + const client = manager.createClient({ + path, + slaveId, + }) + + const foundClient = manager.findClient({ + path, + slaveId, + }) + + assert.notEqual(foundClient, undefined, 'Searched for client should be defined') + assert.equal(client, foundClient, 'Should be the same client') + assert.equal(client.slaveId, foundClient.slaveId, 'Should have the same slaveId') + assert.equal(client.socket.localAddress, foundClient.socket.localAddress, 'Should have the same localAddress') + assert.equal(client.socket.localPort, foundClient.socket.localPort, 'Should have the same localAddress') + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should get accurate socketCount & clientCount', () => { + addMultipleSlavesById(generateRandomPath(), 10) + addMultipleSlavesById(generateRandomPath(), 15) + + assert.equal(manager.clientCount, 25, 'Number of clients should be 25') + assert.equal(manager.socketCount, 2, 'Number of sockets should be 2') + }) + + it('should filterClientsBySocket', () => { + + const testPath = generateRandomPath() + const testNumberOfClients = 15 + + addMultipleSlavesById(generateRandomPath(), 10) + addMultipleSlavesById(generateRandomPath(), 5) + addMultipleSlavesById(generateRandomPath(), 2) + addMultipleSlavesById(testPath, testNumberOfClients) + + const clients = manager.filterClientsBySocket({path: testPath}) + + assert.equal(clients.size, testNumberOfClients, 'The number of clients should match the socket it is assigned to') + + const socket = manager.findSocket({ + path: testPath + }) + for(const [_, client] of clients){ + assert.equal(socket, client.socket, 'Client socket should be the same socket for all clients') + } + }) + + it('should findOrCreateClient but not duplicate', () => { + const testPath = generateRandomPath() + const testSlaveId = 6 + + const client_1 = manager.findOrCreateClient({ + path: testPath, + slaveId: testSlaveId + }) + + const client_2 = manager.findOrCreateClient({ + path: testPath, + slaveId: testSlaveId + }) + + assert.equal(client_1 instanceof ModbusRTUClient, true, 'Client 1 should be an instance of ModbusRTUClient') + assert.equal(client_2 instanceof ModbusRTUClient, true, 'Client 2 should be an instance of ModbusRTUClient') + assert.equal(client_1, client_2, 'Client 1 should be the same reference as Client 2') + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should findSocket', () => { + const testPath = generateRandomPath() + + addClient(testPath) + + const socket = manager.findSocket({ + path: testPath + }) + + assert.equal(socket instanceof SerialPort, true, 'Socket should be an instance of SerialPort') + }) + + it('should findOrCreateSocket', () => { + const testPath = generateRandomPath() + + const socket = manager.findOrCreateSocket({ + path: testPath + }) + + assert.equal(socket instanceof SerialPort, true, 'Socket should be an instance of SerialPort') + }) + + it('should removeSocket and all clients bound to that socket', () => { + const testPath = generateRandomPath() + const testNumberOfClients = 15 + + addMultipleSlavesById(generateRandomPath(), 10) + addMultipleSlavesById(generateRandomPath(), 5) + addMultipleSlavesById(generateRandomPath(), 2) + addMultipleSlavesById(testPath, testNumberOfClients) + + let clients = manager.filterClientsBySocket({ + path: testPath + }) + + assert.equal(clients.size, testNumberOfClients, 'The number of clients should match the socket it is assigned to') + + manager.removeSocket({ + path: testPath + }) + + clients = manager.filterClientsBySocket({ + path: testPath + }) + + assert.equal(clients.size, 0, 'The number of clients after removing the socket should be zero') + + }) + + it('should removeSocket and manually reject all requests for the clients', () => { + // TODO + }) + + it('should removeClient and manually reject all of the clients requests', () => { + // TODO + }) + + it('should remove clients by socket but not remove the original socket', () => { + const testPath = generateRandomPath() + const testNumberOfClients = 15 + + addMultipleSlavesById(generateRandomPath(), 10) + addMultipleSlavesById(generateRandomPath(), 5) + addMultipleSlavesById(generateRandomPath(), 2) + addMultipleSlavesById(testPath, testNumberOfClients) + + let clients = manager.filterClientsBySocket({ + path: testPath + }) + + assert.equal(clients.size, testNumberOfClients, 'The number of clients should match the socket it is assigned to') + + manager.removeClientsBySocket({ + path: testPath + }) + + clients = manager.filterClientsBySocket({ + path: testPath + }) + + assert.equal(clients.size, 0, 'The number of clients should be zero') + + const socket = manager.findSocket({ + path: testPath + }) + + assert.equal(socket instanceof SerialPort, true, 'Socket should be an instance of SerialPort') + }) + + it('should remove all unused sockets', () => { + const testPath = generateRandomPath() + + addMultipleSlavesById(generateRandomPath(), 10) + addMultipleSlavesById(generateRandomPath(), 5) + addMultipleSlavesById(generateRandomPath(), 2) + + manager.createSocket({ + path: testPath + }) + + let clients = manager.filterClientsBySocket({ + path: testPath + }) + + assert.equal(clients.size, 0, 'The number of clients should be zero') + + manager.removeAllUnusedSockets() + + const socket = manager.findSocket({ + path: testPath + }) + + assert.equal(socket, undefined, 'Socket should not be found') + }) + + it('should create a client where the socket only has a maximum of 255 listeners', () => { + checkEmptyManager() + + const client = addClient() + + assert.equal(client.socket.getMaxListeners(), 255, 'Number of listeners should equal 255') + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should only add one socket for clients with the same host & port', () => { + checkEmptyManager() + + const testPath = generateRandomPath() + + const numClients = 255; + + for (let slaveId = 1; slaveId < numClients; slaveId++){ + addClient(testPath, slaveId) + } + + assert.equal(manager.clientCount, numClients - 1, `Number of clients should be ${numClients}`) + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + it('should not allow duplicate clients to be created', () => { + checkEmptyManager() + + const testPath = generateRandomPath() + const slaveId = 1; + + manager.createClient({ path: testPath, slaveId }) + + assert.throws(() => manager.createClient({ host, port, slaveId })) + + assert.equal(manager.clientCount, 1, 'Number of clients should be one') + assert.equal(manager.socketCount, 1, 'Number of sockets should be one') + }) + + +}) From b9ba9c581383e39c7f3ace2427083b70c35e9313 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 22:13:26 -0600 Subject: [PATCH 12/19] max listeners for SerialPort should be 255 --- src/client-manager/modbus-rtu-client-manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client-manager/modbus-rtu-client-manager.ts b/src/client-manager/modbus-rtu-client-manager.ts index b82811f..7cd0517 100644 --- a/src/client-manager/modbus-rtu-client-manager.ts +++ b/src/client-manager/modbus-rtu-client-manager.ts @@ -97,6 +97,10 @@ export default class ModbusRTUClientManager { } = rtuInfo const socket = new SerialPort(path, options) + // set maximum listeners to the maximum number of clients for + // a single rtu master + socket.setMaxListeners(255) + const socketId = this.marshalSocketId(rtuInfo) this.sockets.set(socketId, socket) From 1d2a8d9465d483900afee982e4d9939811e8c8ac Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 22:32:27 -0600 Subject: [PATCH 13/19] do not open socket for ModbusRTUClientManager tests --- test/rtu-client-manager.test.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/rtu-client-manager.test.js b/test/rtu-client-manager.test.js index 01b2ddb..5099666 100644 --- a/test/rtu-client-manager.test.js +++ b/test/rtu-client-manager.test.js @@ -26,7 +26,7 @@ describe('ModbusRTUClientManager Tests.', () => { defaultSerialPortOptions = { baudRate: 9600, dataBits: 8, - autoOpen: false + autoOpen: false, } }) @@ -66,6 +66,7 @@ describe('ModbusRTUClientManager Tests.', () => { manager.createClient({ path, slaveId, + ...defaultSerialPortOptions }) assert.equal(manager.clientCount, 1, 'Number of clients should be one') @@ -80,6 +81,7 @@ describe('ModbusRTUClientManager Tests.', () => { const client = manager.createClient({ path, slaveId, + ...defaultSerialPortOptions }) const foundClient = manager.findClient({ @@ -133,12 +135,14 @@ describe('ModbusRTUClientManager Tests.', () => { const client_1 = manager.findOrCreateClient({ path: testPath, - slaveId: testSlaveId + slaveId: testSlaveId, + ...defaultSerialPortOptions }) const client_2 = manager.findOrCreateClient({ path: testPath, - slaveId: testSlaveId + slaveId: testSlaveId, + ...defaultSerialPortOptions }) assert.equal(client_1 instanceof ModbusRTUClient, true, 'Client 1 should be an instance of ModbusRTUClient') @@ -165,7 +169,8 @@ describe('ModbusRTUClientManager Tests.', () => { const testPath = generateRandomPath() const socket = manager.findOrCreateSocket({ - path: testPath + path: testPath, + ...defaultSerialPortOptions }) assert.equal(socket instanceof SerialPort, true, 'Socket should be an instance of SerialPort') @@ -246,7 +251,8 @@ describe('ModbusRTUClientManager Tests.', () => { addMultipleSlavesById(generateRandomPath(), 2) manager.createSocket({ - path: testPath + path: testPath, + ...defaultSerialPortOptions }) let clients = manager.filterClientsBySocket({ @@ -296,7 +302,11 @@ describe('ModbusRTUClientManager Tests.', () => { const testPath = generateRandomPath() const slaveId = 1; - manager.createClient({ path: testPath, slaveId }) + manager.createClient({ + path: testPath, + slaveId, + ...defaultSerialPortOptions + }) assert.throws(() => manager.createClient({ host, port, slaveId })) From 9c2a94c504d89297dda7014bd115bb9f3aea67be Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 22:40:46 -0600 Subject: [PATCH 14/19] removed spread operator from tests - tests do not have polyfills for older versions of node.js, so reverted the spread operator to Object.assign --- test/rtu-client-manager.test.js | 51 ++++++++++++++------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/test/rtu-client-manager.test.js b/test/rtu-client-manager.test.js index 5099666..16de11a 100644 --- a/test/rtu-client-manager.test.js +++ b/test/rtu-client-manager.test.js @@ -36,11 +36,11 @@ describe('ModbusRTUClientManager Tests.', () => { function addClient(path = defaultSerialPortPath, slaveId = 1, options = defaultSerialPortOptions) { - const client = manager.findOrCreateClient({ + const fullOptions = Object.assign({ path, slaveId, - ...options - }) + },options) + const client = manager.findOrCreateClient(fullOptions) assert.equal(client instanceof ModbusRTUClient, true) return client @@ -63,11 +63,11 @@ describe('ModbusRTUClientManager Tests.', () => { const path = defaultSerialPortPath, slaveId = 1 - manager.createClient({ + + manager.createClient(Object.assign({ path, slaveId, - ...defaultSerialPortOptions - }) + },defaultSerialPortOptions)) assert.equal(manager.clientCount, 1, 'Number of clients should be one') assert.equal(manager.socketCount, 1, 'Number of sockets should be one') @@ -78,11 +78,10 @@ describe('ModbusRTUClientManager Tests.', () => { const path = defaultSerialPortPath, slaveId = 1 - const client = manager.createClient({ + const client = manager.createClient(Object.assign({ path, slaveId, - ...defaultSerialPortOptions - }) + },defaultSerialPortOptions)) const foundClient = manager.findClient({ path, @@ -133,17 +132,14 @@ describe('ModbusRTUClientManager Tests.', () => { const testPath = generateRandomPath() const testSlaveId = 6 - const client_1 = manager.findOrCreateClient({ + const options = Object.assign({ path: testPath, slaveId: testSlaveId, - ...defaultSerialPortOptions - }) + },defaultSerialPortOptions); - const client_2 = manager.findOrCreateClient({ - path: testPath, - slaveId: testSlaveId, - ...defaultSerialPortOptions - }) + const client_1 = manager.findOrCreateClient(options) + + const client_2 = manager.findOrCreateClient(options) assert.equal(client_1 instanceof ModbusRTUClient, true, 'Client 1 should be an instance of ModbusRTUClient') assert.equal(client_2 instanceof ModbusRTUClient, true, 'Client 2 should be an instance of ModbusRTUClient') @@ -168,10 +164,9 @@ describe('ModbusRTUClientManager Tests.', () => { it('should findOrCreateSocket', () => { const testPath = generateRandomPath() - const socket = manager.findOrCreateSocket({ - path: testPath, - ...defaultSerialPortOptions - }) + const socket = manager.findOrCreateSocket(Object.assign({ + path: testPath + }, defaultSerialPortOptions)) assert.equal(socket instanceof SerialPort, true, 'Socket should be an instance of SerialPort') }) @@ -250,10 +245,9 @@ describe('ModbusRTUClientManager Tests.', () => { addMultipleSlavesById(generateRandomPath(), 5) addMultipleSlavesById(generateRandomPath(), 2) - manager.createSocket({ - path: testPath, - ...defaultSerialPortOptions - }) + manager.createSocket(Object.assign({ + path: testPath + }, defaultSerialPortOptions)) let clients = manager.filterClientsBySocket({ path: testPath @@ -302,11 +296,10 @@ describe('ModbusRTUClientManager Tests.', () => { const testPath = generateRandomPath() const slaveId = 1; - manager.createClient({ + manager.createClient(Object.assign({ path: testPath, - slaveId, - ...defaultSerialPortOptions - }) + slaveId + },defaultSerialPortOptions)) assert.throws(() => manager.createClient({ host, port, slaveId })) From 4530b22abb233b71b7a65b942631b2d6e2aec81f Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 23:02:41 -0600 Subject: [PATCH 15/19] added examples for ModbusTCPClientManager --- .../javascript/tcp/ModbusTCPClientManager.js | 33 +++++++++++++++++++ .../typescript/tcp/ModbusTCPClientManager.ts | 32 ++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 examples/javascript/tcp/ModbusTCPClientManager.js create mode 100644 examples/typescript/tcp/ModbusTCPClientManager.ts diff --git a/examples/javascript/tcp/ModbusTCPClientManager.js b/examples/javascript/tcp/ModbusTCPClientManager.js new file mode 100644 index 0000000..819f73e --- /dev/null +++ b/examples/javascript/tcp/ModbusTCPClientManager.js @@ -0,0 +1,33 @@ +const modbus = require('../../..') + +const manager = new modbus.ModbusTCPClientManager() + +const socket = manager.findOrCreateSocket({ + host: 'localhost', + port: 5052, +}) + +socket.on('connect', () => { + console.log('SOCKET CONNECTED') + + setInterval(() => { + for(const [clientId, client] of manager.clients){ + client + .readCoils(0, 5) + .then(({response}) => console.log(response.body.valuesAsArray)) + .catch(console.error) + } + }, 1000) +}) + +for(let i = 0; i < 50; i++) { + manager.findOrCreateClient({ + host: 'localhost', + port: 5052, + slaveId: i + }) +} + + +console.log(manager.clientCount) // should be 50 +console.log(manager.socketCount) // should be 1 \ No newline at end of file diff --git a/examples/typescript/tcp/ModbusTCPClientManager.ts b/examples/typescript/tcp/ModbusTCPClientManager.ts new file mode 100644 index 0000000..e2541af --- /dev/null +++ b/examples/typescript/tcp/ModbusTCPClientManager.ts @@ -0,0 +1,32 @@ +import Modbus from '../../../dist/modbus' + +const manager = new Modbus.ModbusTCPClientManager() + +const socket = manager.findOrCreateSocket({ + host: 'localhost', + port: 5052 +}) + +socket.on('connect', () => { + console.log('SOCKET CONNECTED') + + setInterval(() => { + for (const [clientId, client] of manager.clients) { + client + .readCoils(0, 5) + .then(({ response }) => console.log(response.body.valuesAsArray)) + .catch(console.error) + } + }, 1000) +}) + +for (let i = 0; i < 50; i++) { + manager.findOrCreateClient({ + host: 'localhost', + port: 5052, + slaveId: i + }) +} + +console.log(manager.clientCount) // should be 50 +console.log(manager.socketCount) // should be 1 From 689f6c0b427ae609fd08fe11fa7a90cdf096acf6 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 23:29:39 -0600 Subject: [PATCH 16/19] increased randomness of path --- test/rtu-client-manager.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rtu-client-manager.test.js b/test/rtu-client-manager.test.js index 16de11a..1cb2981 100644 --- a/test/rtu-client-manager.test.js +++ b/test/rtu-client-manager.test.js @@ -31,7 +31,7 @@ describe('ModbusRTUClientManager Tests.', () => { }) function generateRandomPath() { - return defaultSerialPortPath + Math.floor(Math.random() * 100) + return defaultSerialPortPath + Math.floor(Math.random() * Math.pow(10, 10)) } function addClient(path = defaultSerialPortPath, slaveId = 1, options = defaultSerialPortOptions) { From c33bc468041efa67fe967310fd605b437c432b82 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Mon, 3 Feb 2020 23:32:53 -0600 Subject: [PATCH 17/19] ensure random path never creates the same path --- test/rtu-client-manager.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/rtu-client-manager.test.js b/test/rtu-client-manager.test.js index 1cb2981..a5e386c 100644 --- a/test/rtu-client-manager.test.js +++ b/test/rtu-client-manager.test.js @@ -9,6 +9,8 @@ const { } = require('../dist/modbus') const SerialPort = require('serialport') +let randomCount = 10; + describe('ModbusRTUClientManager Tests.', () => { /** * @type {ModbusRTUClientManager} @@ -31,7 +33,8 @@ describe('ModbusRTUClientManager Tests.', () => { }) function generateRandomPath() { - return defaultSerialPortPath + Math.floor(Math.random() * Math.pow(10, 10)) + randomCount++ + return defaultSerialPortPath + randomCount } function addClient(path = defaultSerialPortPath, slaveId = 1, options = defaultSerialPortOptions) { From c9c286cb399a4967f13baa0ecc0fb4585ec0d1bd Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Tue, 4 Feb 2020 20:08:10 -0600 Subject: [PATCH 18/19] fixed ErrorCode name mismatch --- src/client-manager/modbus-tcp-client-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client-manager/modbus-tcp-client-manager.ts b/src/client-manager/modbus-tcp-client-manager.ts index 11379e7..110c325 100644 --- a/src/client-manager/modbus-tcp-client-manager.ts +++ b/src/client-manager/modbus-tcp-client-manager.ts @@ -20,7 +20,7 @@ interface ITCPSlaveInfo extends ITCPInfo { enum ErrorCode { DUPLICATE_SOCKET = 'DUPLICATE_SOCKET', - DUPLICATE_CLIENT = 'DUPLICATE_SOCKET' + DUPLICATE_CLIENT = 'DUPLICATE_CLIENT' } export default class ModbusTCPClientManager { From c2c1fc071ee6d34116da765a60ef324d2139a710 Mon Sep 17 00:00:00 2001 From: Alexander Buczynsky Date: Tue, 4 Feb 2020 20:09:09 -0600 Subject: [PATCH 19/19] findSocketsWithoutClients uses MapUtils.Filter --- .../modbus-rtu-client-manager.ts | 20 +++++++++---------- .../modbus-tcp-client-manager.ts | 20 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/client-manager/modbus-rtu-client-manager.ts b/src/client-manager/modbus-rtu-client-manager.ts index 7cd0517..314e656 100644 --- a/src/client-manager/modbus-rtu-client-manager.ts +++ b/src/client-manager/modbus-rtu-client-manager.ts @@ -161,17 +161,15 @@ export default class ModbusRTUClientManager { * Finds sockets that do not have any clients using it */ private findSocketsWithoutClients () { - const unusedSocketMap = new Map() - - for (const [socketId, socket] of this.sockets) { - const rtuInfo = this.unmarshalSocketId(socketId) - const clients = this.filterClientsBySocket(rtuInfo) - if (clients.size === 0) { - unusedSocketMap.set(socketId, socket) - } - } - - return unusedSocketMap + return MapUtils.Filter(this.sockets, + ( + ([socketId]) => { + const rtuInfo = this.unmarshalSocketId(socketId) + const clients = this.filterClientsBySocket(rtuInfo) + return clients.size === 0 + } + ) + ) } private marshalSocketId ({ path }: IRTUInfo): SocketId { diff --git a/src/client-manager/modbus-tcp-client-manager.ts b/src/client-manager/modbus-tcp-client-manager.ts index 110c325..c3ac3e8 100644 --- a/src/client-manager/modbus-tcp-client-manager.ts +++ b/src/client-manager/modbus-tcp-client-manager.ts @@ -174,17 +174,15 @@ export default class ModbusTCPClientManager { * Finds sockets that do not have any clients using it */ private findSocketsWithoutClients () { - const unusedSocketMap = new Map() - - for (const [socketId, socket] of this.sockets) { - const { host, port } = this.unmarshalSocketId(socketId) - const clients = this.filterClientsBySocket({ host, port }) - if (clients.size === 0) { - unusedSocketMap.set(socketId, socket) - } - } - - return unusedSocketMap + return MapUtils.Filter(this.sockets, + ( + ([socketId]) => { + const rtuInfo = this.unmarshalSocketId(socketId) + const clients = this.filterClientsBySocket(rtuInfo) + return clients.size === 0 + } + ) + ) } private marshalSocketId ({ host, port }: ITCPInfo): SocketId {