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 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/client-manager/modbus-rtu-client-manager.ts b/src/client-manager/modbus-rtu-client-manager.ts new file mode 100644 index 0000000..314e656 --- /dev/null +++ b/src/client-manager/modbus-rtu-client-manager.ts @@ -0,0 +1,197 @@ +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) + + // 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) + + 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 () { + 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 { + 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) + } + } +} 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..c3ac3e8 --- /dev/null +++ b/src/client-manager/modbus-tcp-client-manager.ts @@ -0,0 +1,214 @@ +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_CLIENT' +} + +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 () { + 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 { + 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) + } + } +} 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 = (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)) + } +} diff --git a/src/modbus-client.ts b/src/modbus-client.ts index 8a9b123..57a3ae0 100644 --- a/src/modbus-client.ts +++ b/src/modbus-client.ts @@ -70,6 +70,20 @@ export default abstract class MBClient { + + 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 diff --git a/test/rtu-client-manager.test.js b/test/rtu-client-manager.test.js new file mode 100644 index 0000000..a5e386c --- /dev/null +++ b/test/rtu-client-manager.test.js @@ -0,0 +1,314 @@ +'use strict' + +/* global describe, it, beforeEach */ + +const assert = require('assert') +const { + ModbusRTUClientManager, + ModbusRTUClient, +} = require('../dist/modbus') +const SerialPort = require('serialport') + +let randomCount = 10; + +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() { + randomCount++ + return defaultSerialPortPath + randomCount + } + + function addClient(path = defaultSerialPortPath, slaveId = 1, options = defaultSerialPortOptions) { + + const fullOptions = Object.assign({ + path, + slaveId, + },options) + const client = manager.findOrCreateClient(fullOptions) + + 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(Object.assign({ + path, + slaveId, + },defaultSerialPortOptions)) + + 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(Object.assign({ + path, + slaveId, + },defaultSerialPortOptions)) + + 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 options = Object.assign({ + 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') + 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(Object.assign({ + path: testPath + }, defaultSerialPortOptions)) + + 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(Object.assign({ + path: testPath + }, defaultSerialPortOptions)) + + 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(Object.assign({ + path: testPath, + slaveId + },defaultSerialPortOptions)) + + 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') + }) + + +}) diff --git a/test/tcp-client-manager.test.js b/test/tcp-client-manager.test.js new file mode 100644 index 0000000..58dee3e --- /dev/null +++ b/test/tcp-client-manager.test.js @@ -0,0 +1,305 @@ +'use strict' + +/* global describe, it, beforeEach */ + +const assert = require('assert') +const { + ModbusTCPClientManager, + ModbusTCPClient, +} = require('../dist/modbus') +const { Socket } = require('net') + +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); slaveId++){ + 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', 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) + + manager.createSocket({ + host: testHost, + port: testPort + }) + + 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 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') + }) + + +})