From 8b04dcd6a2816ad73699138e9d3cfcf208e83993 Mon Sep 17 00:00:00 2001 From: tagyoureit Date: Sun, 4 Apr 2021 21:09:33 -0700 Subject: [PATCH] bi-directional setup for REM/njsPC --- config/Config.ts | 10 +++ controller/nixie/chemistry/ChemController.ts | 2 +- defaultConfig.json | 19 +--- web/Server.ts | 92 ++++++++++++++++---- web/services/config/Config.ts | 38 +++++++- 5 files changed, 126 insertions(+), 35 deletions(-) diff --git a/config/Config.ts b/config/Config.ts index aa5d1147..031dec38 100755 --- a/config/Config.ts +++ b/config/Config.ts @@ -130,5 +130,15 @@ class Config { }); } } + public setInterface(obj: any){ + let interfaces: any = this._cfg.web.interfaces; + for (var i in interfaces) { + if (interfaces[i].uuid === obj.uuid) { + interfaces[i] = obj; + break; + } + } + this.update(); + } } export const config: Config = new Config(); diff --git a/controller/nixie/chemistry/ChemController.ts b/controller/nixie/chemistry/ChemController.ts index c81ec8e1..1ede615b 100644 --- a/controller/nixie/chemistry/ChemController.ts +++ b/controller/nixie/chemistry/ChemController.ts @@ -383,7 +383,7 @@ export class NixieChemController extends NixieEquipment { // We are not processing Homegrown at this point. // Check each piece of equipment to make sure it is doing its thing. this.calculateSaturationIndex(); - await this.processAlarms(schem); + this.processAlarms(schem); if (this.chem.ph.enabled) await this.ph.checkDosing(this.chem, schem.ph); if (this.chem.orp.enabled) await this.orp.checkDosing(this.chem, schem.orp); } diff --git a/defaultConfig.json b/defaultConfig.json index e3d7061c..35f7ef44 100755 --- a/defaultConfig.json +++ b/defaultConfig.json @@ -1,7 +1,7 @@ { "controller": { "comms": { - "enabled": true, + "enabled": true, "rs485Port": "/dev/ttyUSB0", "mockPort": false, "netConnect": false, @@ -45,7 +45,7 @@ "enabled": false }, "ssdp": { - "enabled": false + "enabled": true } }, "services": {}, @@ -145,7 +145,6 @@ "changesOnly": true } }, - "rem": { "name": "Relay Equipment Manager", "type": "rem", @@ -167,18 +166,6 @@ "reconnection": true, "reconnectionDelayMax": 20000 } - }, - "rulesManager": { - "name": "Rules Manager", - "type": "http", - "enabled": false, - "fileName": "rulesManager.json", - "globals": {}, - "options": { - "host": "localhost", - "port": 4200 - }, - "uuid": "65ee2581-e652-427e-99c6-17c6aa02a6e3" } } }, @@ -251,7 +238,7 @@ "enabled": true, "level": "info", "captureForReplay": false, - "logToFile": false + "logToFile": false } }, "appVersion": "0.0.1" diff --git a/web/Server.ts b/web/Server.ts index 72a8d868..cf9bf9ad 100755 --- a/web/Server.ts +++ b/web/Server.ts @@ -142,8 +142,7 @@ export class WebServer { this._servers[s] = undefined; } catch (err) { console.log(`Error stopping server ${s}: ${err.message}`); } } - } catch (err) {`Error stopping servers`} - + } catch (err) { `Error stopping servers` } } private getInterface() { const networkInterfaces = os.networkInterfaces(); @@ -171,6 +170,12 @@ export class WebServer { public findServer(name: string): ProtoServer { return this._servers.find(elem => elem.name === name); } public findServersByType(type: string) { return this._servers.filter(elem => elem.type === type); } public findServerByGuid(uuid: string) { return this._servers.find(elem => elem.uuid === uuid); } + public async updateServerInterface(obj: any) { + config.setInterface(obj); + let srv = this.findServerByGuid(obj.uuid); + if (typeof srv !== 'undefined') await srv.stopAsync(); + if (obj.enabled) srv.init(obj); + } } class ProtoServer { constructor(name: string, type: string) { this.name = name; this.type = type; } @@ -182,6 +187,7 @@ class ProtoServer { public get isConnected() { return this.isRunning; } public emitToClients(evt: string, ...data: any) { } public emitToChannel(channel: string, evt: string, ...data: any) { } + public init(obj: any) { }; public async stopAsync() { } protected _dev: boolean = process.env.NODE_ENV !== 'production'; // todo: how do we know if the client is using IPv4/IPv6? @@ -374,10 +380,15 @@ export class HttpServer extends ProtoServer { this.isRunning = true; } } + public addListenerOnce(event: any, f: (data: any) => void) { + for (let i = 0; i < this._sockets.length; i++) { + this._sockets[i].once(event, f); + } + } } export class HttpsServer extends HttpServer { public server: https.Server; - + public init(cfg) { // const auth = require('http-auth'); this.uuid = cfg.uuid; @@ -385,17 +396,17 @@ export class HttpsServer extends HttpServer { try { this.app = express(); // Enable Authentication (if configured) -/* if (cfg.authentication === 'basic') { - let basic = auth.basic({ - realm: "nodejs-poolController.", - file: path.join(process.cwd(), cfg.authFile) - }) - this.app.use(function(req, res, next) { - (auth.connect(basic))(req, res, next); - }); - } */ - if (cfg.sslKeyFile === '' || cfg.sslCertFile === '' || !fs.existsSync(path.join(process.cwd(), cfg.sslKeyFile)) || !fs.existsSync(path.join(process.cwd(), cfg.sslCertFile))) { - logger.warn(`HTTPS not enabled because key or crt file is missing.`); + /* if (cfg.authentication === 'basic') { + let basic = auth.basic({ + realm: "nodejs-poolController.", + file: path.join(process.cwd(), cfg.authFile) + }) + this.app.use(function(req, res, next) { + (auth.connect(basic))(req, res, next); + }); + } */ + if (cfg.sslKeyFile === '' || cfg.sslCertFile === '' || !fs.existsSync(path.join(process.cwd(), cfg.sslKeyFile)) || !fs.existsSync(path.join(process.cwd(), cfg.sslCertFile))) { + logger.warn(`HTTPS not enabled because key or crt file is missing.`); return; } let opts = { @@ -834,6 +845,55 @@ export class REMInterfaceServer extends ProtoServer { this.uuid = cfg.uuid; if (cfg.enabled) { this.initSockets(); + setTimeout(async () => { + try { + await this.initConnection(); + } + catch (err) { + logger.error(`Error establishing bi-directional Nixie/REM connection: ${err}`) + } + }, 5000); + } + } + private async initConnection() { + try { + // find HTTP server + return new Promise(async (resolve, reject) => { + // First, send the connection info for njsPC and see if a connection exists. + let url = '/config/checkconnection/'; + // can & should extend for https/username-password/ssl + let data: any = { type: "njspc", isActive: true, id: null, name: "njsPC - automatic", protocol: "http:", ipAddress: webApp.ip(), port: config.getSection('web').servers.http.port || 4200, userName: "", password: "", sslKeyFile: "", sslCertFile: "" } + let result = await this.putApiService(url, data, 5000); + // If the result code is > 200 we have an issue. (-1 is for timeout) + if (result.status.code > 200 || result.status.code < 0) return reject(new Error(`initConnection: ${result.error.message}`)); + + // The passed connection has been setup/verified; now test for emit + // if this fails, it could be because the remote connection is disabled. We will not + // automatically re-enable it + url = '/config/checkemit' + data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id } + // wait for REM server to finish resetting + setTimeout(async () => { + try { + let _tmr = setTimeout(() => { return reject(new Error(`initConnection: No socket response received. Check REM→njsPC communications.`)) }, 2000); + let srv: HttpServer = webApp.findServer('http') as HttpServer; + srv.addListenerOnce('/checkemit', (data: any) => { + // if we receive the emit, data will work both ways. + // console.log(data); + clearTimeout(_tmr); + logger.info(`REM bi-directional communications established.`) + return resolve(); + }); + result = await this.putApiService(url, data, 1000); + // If the result code is > 200 we have an issue. + if (result.status.code > 200) return reject(new Error(`initConnection: ${result.error.message}`)); + } + catch (err) {reject(new Error(`initConnection setTimeout: ${result.error.message}`));} + }, 3000); + }); + } + catch (err) { + logger.error(`Error with REM Interface Server initConnection: ${err}`) } } public async stopAsync() { @@ -850,7 +910,7 @@ export class REMInterfaceServer extends ProtoServer { private _sockets: socketio.Socket[] = []; private async sendClientRequest(method: string, url: string, data?: any, timeout:number = 10000): Promise { try { - + let ret = new InterfaceServerResponse(); let opts = extend(true, { headers: {} }, this.cfg.options); if ((typeof opts.hostname === 'undefined' || !opts.hostname) && (typeof opts.host === 'undefined' || !opts.host || opts.host === '*')) { @@ -898,8 +958,8 @@ export class REMInterfaceServer extends ProtoServer { }); req.on('abort', () => { logger.warn('Request Aborted'); reject(new Error('Request Aborted.')); }); req.end(sbody); - logger.verbose(`REM server request returned. ${opts.method} ${opts.path} ${sbody}`); }).catch((err) => { logger.error(`Error Sending REM Request: ${opts.method} ${url} ${err.message}`); ret.error = err; }); + logger.verbose(`REM server request returned. ${opts.method} ${opts.path} ${sbody}`); if (ret.status.code > 200) { // We have an http error so let's parse it up. try { diff --git a/web/services/config/Config.ts b/web/services/config/Config.ts index b427fd33..534630b9 100755 --- a/web/services/config/Config.ts +++ b/web/services/config/Config.ts @@ -23,6 +23,7 @@ import { utils } from "../../../controller/Constants"; import { state } from "../../../controller/State"; import { stopPacketCaptureAsync, startPacketCapture } from '../../../app'; import { conn } from "../../../controller/comms/Comms"; +import { webApp } from "../../Server"; export class ConfigRoute { public static initRoutes(app: express.Application) { @@ -218,6 +219,7 @@ export class ConfigRoute { flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(), acidTypes: sys.board.valueMaps.acidTypes.toArray(), remServers: await sys.ncp.getREMServers(), + interfaces: config.getSection('web.interfaces.rem'), dosingStatus: sys.board.valueMaps.chemControllerDosingStatus.toArray(), siCalcTypes: sys.board.valueMaps.siCalcTypes.toArray(), alarms, @@ -259,6 +261,26 @@ export class ConfigRoute { } return res.status(200).send(opts); }); + app.get('/app/all/', (req, res) => { + let opts = config.getSection(); + return res.status(200).send(opts); + }); + app.get('/app/options/interfaces', (req, res) => { + // todo: move bytevaluemaps out to a proper location; add additional definitions + let opts = { + interfaces: config.getSection('web.interfaces'), + types: [ + {name: 'rem', desc: 'Relay Equipment Manager'}, + {name: 'mqtt', desc: 'MQTT'} + ], + protocols: [ + { val: 0, name: 'http://', desc: 'http://' }, + { val: 1, name: 'https://', desc: 'https://' }, + { val: 2, name: 'mqtt://', desc: 'mqtt://' }, + ] + } + return res.status(200).send(opts); + }); app.get('/config/options/tempSensors', (req, res) => { let opts = { tempUnits: sys.board.valueMaps.tempUnits.toArray(), @@ -276,6 +298,13 @@ export class ConfigRoute { }); /******* END OF CONFIGURATION PICK LISTS/REFERENCES AND VALIDATION ***********/ /******* ENDPOINTS FOR MODIFYING THE OUTDOOR CONTROL PANEL SETTINGS **********/ + app.put('/config/rem', async (req, res, next)=>{ + try { + config.setSection('web.interfaces.rem', req.body); + + } + catch (err) {next(err);} + }) app.put('/config/tempSensors', async (req, res, next) => { try { await sys.board.system.setTempSensorsAsync(req.body); @@ -741,6 +770,13 @@ export class ConfigRoute { sys.board.reloadConfig(); return res.status(200).send('OK'); }); + app.put('/app/interface', async (req, res, next) => { + try{ + await webApp.updateServerInterface(req.body); + return res.status(200).send('OK'); + } + catch (err) {next(err);} + }); app.get('/app/config/startPacketCapture', (req, res) => { startPacketCapture(true); return res.status(200).send('OK'); @@ -759,7 +795,5 @@ export class ConfigRoute { app.get('/app/config/:section', (req, res) => { return res.status(200).send(config.getSection(req.params.section)); }); - - } } \ No newline at end of file