Compare commits

...

18 Commits

Author SHA1 Message Date
681856104d Merge pull request 'changed colours, description based on s88' (#2) from dev-Rene into main
Reviewed-on: #2
2025-10-16 13:22:56 +00:00
znetsixe
e0526250c2 changed colours, description based on s88 2025-10-14 13:52:18 +02:00
c0e4539b50 Merge pull request 'dev-Rene' (#1) from dev-Rene into main
Reviewed-on: #1
2025-10-06 14:15:42 +00:00
znetsixe
426d45890f ok 2025-10-05 07:56:35 +02:00
znetsixe
8c59a921d5 syncing 2025-10-05 07:55:23 +02:00
Rene De ren
15501e8b1d updates from laptop 2025-10-03 15:33:37 +02:00
znetsixe
b4364094c6 Stable version of machinegroup control 2025-10-02 17:08:41 +02:00
znetsixe
a55c6bdbea fixed pressure updates from machines. Everything seems to be working again. 2025-09-23 15:50:40 +02:00
znetsixe
ac9d1b4fdd added test file 2025-09-23 15:12:01 +02:00
znetsixe
cbc0840f0c added testfile fixing bugs 2025-09-23 15:03:57 +02:00
znetsixe
c62071992d working on a stable version 2025-09-23 11:19:22 +02:00
znetsixe
ffab553f7e physicalPosition 1D update 2025-09-05 16:20:22 +02:00
znetsixe
078a0d80dc updated function for registration of machines 2025-09-04 17:07:18 +02:00
znetsixe
dc1fb500c0 license update 2025-08-07 13:52:56 +02:00
znetsixe
de5652b73d small bug fixes 2025-07-31 09:10:34 +02:00
znetsixe
2aeb876c0d bug fixes 2025-07-02 10:52:37 +02:00
znetsixe
35eb965609 not working yet need to fix child registration? 2025-07-01 17:03:36 +02:00
znetsixe
e6979d06ad updates groupcontrol 2025-06-02 16:55:01 +02:00
14 changed files with 1015 additions and 2691 deletions

102
LICENSE
View File

@@ -1,15 +1,97 @@
MIT License OPENBARE LICENTIE VAN DE EUROPESE UNIE v. 1.2.
EUPL © Europese Unie 2007, 2016
Deze openbare licentie van de Europese Unie („EUPL”) is van toepassing op het werk (zoals hieronder gedefinieerd) dat onder de voorwaarden van deze licentie wordt verstrekt. Elk gebruik van het werk dat niet door deze licentie is toegestaan, is verboden (voor zover dit gebruik valt onder een recht van de houder van het auteursrecht op het werk). Het werk wordt verstrekt onder de voorwaarden van deze licentie wanneer de licentiegever (zoals hieronder gedefinieerd), direct volgend op de kennisgeving inzake het auteursrecht op het werk, de volgende kennisgeving opneemt:
In licentie gegeven krachtens de EUPL
of op een andere wijze zijn bereidheid te kennen heeft gegeven krachtens de EUPL in licentie te geven.
Copyright (c) 2025 Rene De Ren 1.Definities
In deze licentie wordt verstaan onder:
— „de licentie”:de onderhavige licentie;
— „het oorspronkelijke werk”:het werk dat of de software die door de licentiegever krachtens deze licentie wordt verspreid of medegedeeld, en dat/die beschikbaar is als broncode en, in voorkomend geval, ook als uitvoerbare code;
— „bewerkingen”:de werken of software die de licentiehouder kan creëren op grond van het oorspronkelijke werk of wijzigingen ervan. In deze licentie wordt niet gedefinieerd welke mate van wijziging of afhankelijkheid van het oorspronkelijke werk vereist is om een werk als een bewerking te kunnen aanmerken; dat wordt bepaald conform het auteursrecht dat van toepassing is in de in artikel 15 bedoelde staat;
— „het werk”:het oorspronkelijke werk of de bewerkingen ervan;
— „de broncode”:de voor mensen leesbare vorm van het werk, die het gemakkelijkste door mensen kan worden bestudeerd en gewijzigd;
— „de uitvoerbare code”:elke code die over het algemeen is gecompileerd en is bedoeld om door een computer als een programma te worden uitgevoerd;
— „de licentiegever”:de natuurlijke of rechtspersoon die het werk krachtens de licentie verspreidt of mededeelt;
— „bewerker(s)”:elke natuurlijke of rechtspersoon die het werk krachtens de licentie wijzigt of op een andere wijze bijdraagt tot de totstandkoming van een bewerking;
— „de licentiehouder” of „u”:elke natuurlijke of rechtspersoon die het werk onder de voorwaarden van de licentie gebruikt; — „verspreiding” of „mededeling”:het verkopen, geven, uitlenen, verhuren, verspreiden, mededelen, doorgeven, of op een andere wijze online of offline beschikbaar stellen van kopieën van het werk of het verlenen van toegang tot de essentiële functies ervan ten behoeve van andere natuurlijke of rechtspersonen.
Permission is hereby granted, free of charge, to any person obtaining a copy 2.Draagwijdte van de uit hoofde van de licentie verleende rechten
of this software and associated documentation files (the "Software"), to use, De licentiegever verleent u hierbij een wereldwijde, royaltyvrije, niet-exclusieve, voor een sublicentie in aanmerking komende licentie, om voor de duur van het aan het oorspronkelijke werk verbonden auteursrecht, het volgende te doen:
copy, modify, merge, publish, and distribute the Software for **personal, scientific, or educational purposes**, subject to the following conditions: — het werk in alle omstandigheden en voor ongeacht welk doel te gebruiken;
— het werk te verveelvoudigen;
— het werk te wijzigen en op grond van het werk bewerkingen te ontwikkelen;
— het werk aan het publiek mede te delen, waaronder het recht om het werk of kopieën ervan aan het publiek ter beschikking te stellen of te vertonen, en het werk, in voorkomend geval, in het openbaar uit te voeren;
— het werk of kopieën ervan te verspreiden;
— het werk of kopieën ervan uit te lenen en te verhuren;
— de rechten op het werk of op kopieën ervan in sublicentie te geven.
Deze rechten kunnen worden uitgeoefend met gebruikmaking van alle thans bekende of nog uit te vinden media, dragers en formaten, voor zover het toepasselijke recht dit toestaat. In de landen waar immateriële rechten van toepassing zijn, doet de licentiegever afstand van zijn recht op uitoefening van zijn immateriële rechten in de mate die door het toepasselijke recht wordt toegestaan teneinde een doeltreffende uitoefening van de bovenvermelde in licentie gegeven economische rechten mogelijk te maken. De licentiegever verleent de licentiehouder een royaltyvrij, niet-exclusief gebruiksrecht op alle octrooien van de licentiegever, voor zover dit noodzakelijk is om de uit hoofde van deze licentie verleende rechten op het werk te gebruiken.
**Commercial use of the Software or any derivative work is explicitly prohibited without prior written consent from the authors.** 3.Mededeling van de broncode
This includes but is not limited to resale, inclusion in paid products or services, and monetized distribution. De licentiegever kan het werk verstrekken in zijn broncode of als uitvoerbare code. Indien het werk als uitvoerbare code wordt verstrekt, verstrekt de licentiegever bij elke door hem verspreide kopie van het werk tevens een machinaal leesbare kopie van de broncode van het werk of geeft hij in een mededeling, volgende op de bij het werk gevoegde auteursrechtelijke kennisgeving, de plaats aan waar de broncode gemakkelijk en vrij toegankelijk is, zolang de licentiegever het werk blijft verspreiden of mededelen.
Any commercial usage must be governed by a shared license or explicit contractual agreement with the authors.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4.Beperkingen van het auteursrecht
Geen enkele bepaling in deze licentie heeft ten doel de licentiehouder het recht te ontnemen een beroep te doen op een uitzondering op of een beperking van de exclusieve rechten van de rechthebbenden op het werk, of op de uitputting van die rechten of andere toepasselijke beperkingen daarvan.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED... 5.Verplichtingen van de licentiehouder
De verlening van de bovenvermelde rechten is onderworpen aan een aantal aan de licentiehouder opgelegde beperkingen en verplichtingen. Het gaat om de onderstaande verplichtingen.
Attributierecht: de licentiehouder moet alle auteurs-, octrooi- of merkenrechtelijke kennisgevingen onverlet laten alsook alle kennisgevingen die naar de licentie en de afwijzing van garanties verwijzen. De licentiehouder moet een afschrift van deze kennisgevingen en een afschrift van de licentie bij elke kopie van het werk voegen die hij verspreidt of mededeelt. De licentiehouder moet in elke bewerking duidelijk aangeven dat het werk is gewijzigd, en eveneens de datum van wijziging vermelden.
Copyleftclausule: wanneer de licentiehouder kopieën van het oorspronkelijke werk of bewerkingen verspreidt of mededeelt, geschiedt die verspreiding of mededeling onder de voorwaarden van deze licentie of van een latere versie van deze licentie, tenzij het oorspronkelijke werk uitdrukkelijk alleen onder deze versie van de licentie wordt verspreid — bijvoorbeeld door de mededeling „alleen EUPL v. 1.2”. De licentiehouder (die licentiegever wordt) kan met betrekking tot het werk of de bewerkingen geen aanvullende bepalingen of voorwaarden opleggen of stellen die de voorwaarden van de licentie wijzigen of beperken.
Verenigbaarheidsclausule: wanneer de licentiehouder bewerkingen of kopieën ervan verspreidt of mededeelt die zijn gebaseerd op het werk en op een ander werk dat uit hoofde van een verenigbare licentie in licentie is gegeven, kan die verspreiding of mededeling geschieden onder de voorwaarden van deze verenigbare licentie. Voor de toepassing van deze clausule wordt onder „verenigbare licentie” verstaan, de licenties die in het aanhangsel bij deze licentie zijn opgesomd. Indien de verplichtingen van de licentiehouder uit hoofde van de verenigbare licentie in strijd zijn met diens verplichtingen uit hoofde van deze licentie, hebben de verplichtingen van de verenigbare licentie voorrang.
Verstrekking van de broncode: bij de verspreiding of mededeling van kopieën van het werk verstrekt de licentiehouder een machinaal leesbare kopie van de broncode of geeft hij aan waar deze broncode gemakkelijk en vrij toegankelijk is, zolang de licentiehouder het werk blijft verspreiden of mededelen.
Juridische bescherming: deze licentie verleent geen toestemming om handelsnamen, handelsmerken, dienstmerken of namen van de licentiegever te gebruiken, behalve wanneer dit op grond van een redelijk en normaal gebruik noodzakelijk is om de oorsprong van het werk te beschrijven en de inhoud van de auteursrechtelijke kennisgeving te herhalen.
6.Auteursketen
De oorspronkelijke licentiegever garandeert dat hij houder is van het hierbij verleende auteursrecht op het oorspronkelijke werk dan wel dat dit hem in licentie is gegeven en dat hij de bevoegdheid heeft de licentie te verlenen. Elke bewerker garandeert dat hij houder is van het auteursrecht op de door hem aan het werk aangebrachte wijzigingen dan wel dat dit hem in licentie is gegeven en dat hij de bevoegdheid heeft de licentie te verlenen. Telkens wanneer u de licentie aanvaardt, verlenen de oorspronkelijke licentiegever en de opeenvolgende bewerkers u een licentie op hun bijdragen aan het werk onder de voorwaarden van deze licentie.
7.Uitsluiting van garantie
Het werk is een werk in ontwikkeling, dat voortdurend door vele bewerkers wordt verbeterd. Het is een onvoltooid werk, dat bijgevolg nog tekortkomingen of programmeerfouten („bugs”) kan vertonen, die onlosmakelijk verbonden zijn met dit soort ontwikkeling. Om die reden wordt het werk op grond van de licentie verstrekt „zoals het is” en zonder enige garantie met betrekking tot het werk te geven, met inbegrip van, maar niet beperkt tot garanties met betrekking tot de verhandelbaarheid, de geschiktheid voor een specifiek doel, de afwezigheid van tekortkomingen of fouten, de nauwkeurigheid, de eerbiediging van andere intellectuele-eigendomsrechten dan het in artikel 6 van deze licentie bedoelde auteursrecht. Deze uitsluiting van garantie is een essentieel onderdeel van de licentie en een voorwaarde voor de verlening van rechten op het werk.
8.Uitsluiting van aansprakelijkheid
Behoudens in het geval van een opzettelijke fout of directe schade aan natuurlijke personen, is de licentiegever in geen enkel geval aansprakelijk voor ongeacht welke directe of indirecte, materiële of immateriële schade die voortvloeit uit de licentie of het gebruik van het werk, met inbegrip van, maar niet beperkt tot schade als gevolg van het verlies van goodwill, verloren werkuren, een computerdefect of computerfout, het verlies van gegevens, of enige andere commerciële schade, zelfs indien de licentiegever werd gewezen op de mogelijkheid van dergelijke schade. De licentiegever is echter aansprakelijk op grond van de wetgeving inzake productaansprakelijkheid, voor zover deze wetgeving op het werk van toepassing is.
9.Aanvullende overeenkomsten
Bij de verspreiding van het werk kunt u ervoor kiezen een aanvullende overeenkomst te sluiten, waarin de verplichtingen of diensten overeenkomstig deze licentie worden omschreven. Indien deze verplichtingen worden aanvaard, kunt u echter alleen in eigen naam en onder eigen verantwoordelijkheid handelen, en dus niet in naam van de oorspronkelijke licentiegever of een bewerker, en kunt u voorts alleen handelen indien u ermee instemt alle bewerkers schadeloos te stellen, te verdedigen of te vrijwaren met betrekking tot de aansprakelijkheid van of vorderingen tegen deze bewerkers op grond van het feit dat u een garantie of aanvullende aansprakelijkheid hebt aanvaard.
10.Aanvaarding van de licentie
De bepalingen van deze licentie kunnen worden aanvaard door te klikken op het pictogram „Ik ga akkoord”, dat zich bevindt onderaan het venster waarin de tekst van deze licentie is weergegeven, of door overeenkomstig de toepasselijke wetsbepalingen op een soortgelijke wijze met de licentie in te stemmen. Door op dat pictogram te klikken geeft u aan dat u deze licentie en alle voorwaarden ervan ondubbelzinnig en onherroepelijk aanvaardt. Evenzo aanvaardt u onherroepelijk deze licentie en alle voorwaarden ervan door uitoefening van de rechten die u in artikel 2 van deze licentie zijn verleend, zoals het gebruik van het werk, het creëren door u van een bewerking of de verspreiding of mededeling door u van het werk of kopieën ervan.
11.Voorlichting van het publiek
Indien u het werk verspreidt of mededeelt door middel van elektronische communicatiemiddelen (bijvoorbeeld door voor te stellen het werk op afstand te downloaden), moet het distributiekanaal of het medium (bijvoorbeeld een website) het publiek ten minste de gegevens verschaffen die door het toepasselijke recht zijn voorgeschreven met betrekking tot de licentiegever, de licentie en de wijze waarop deze kan worden geraadpleegd, gesloten, opgeslagen en gereproduceerd door de licentiehouder.
12.Einde van de licentie
De licentie en de uit hoofde daarvan verleende rechten eindigen automatisch bij elke inbreuk door de licentiehouder op de voorwaarden van de licentie. Dit einde beëindigt niet de licenties van personen die het werk van de licentiehouder krachtens de licentie hebben ontvangen, mits deze personen zich volledig aan de licentie houden.
13.Overige
Onverminderd artikel 9 vormt de licentie de gehele overeenkomst tussen de partijen met betrekking tot het werk. Indien een bepaling van de licentie volgens het toepasselijke recht ongeldig is of niet uitvoerbaar is, doet dit geen afbreuk aan de geldigheid of uitvoerbaarheid van de licentie in haar geheel. Deze bepaling dient zodanig te worden uitgelegd of gewijzigd dat zij geldig en uitvoerbaar wordt. De Europese Commissie kan, voor zover dit noodzakelijk en redelijk is, versies in andere talen of nieuwe versies van deze licentie of geactualiseerde versies van dit aanhangsel publiceren, zonder de draagwijdte van de uit hoofde van de licentie verleende rechten te beperken. Nieuwe versies van de licentie zullen worden gepubliceerd met een uniek versienummer. Alle door de Europese Commissie goedgekeurde taalversies van deze licentie hebben dezelfde waarde. De partijen kunnen zich beroepen op de taalversie van hun keuze.
14.Bevoegd gerecht
Onverminderd specifieke overeenkomsten tussen de partijen,
— vallen alle geschillen tussen de instellingen, organen en instanties van de Europese Unie, als licentiegeefster, en een licentiehouder in verband met de uitlegging van deze licentie onder de bevoegdheid van het Hof van Justitie van de Europese Unie, conform artikel 272 van het Verdrag betreffende de werking van de Europese Unie,
— vallen alle geschillen tussen andere partijen in verband met de uitlegging van deze licentie onder de uitsluitende bevoegdheid van het bevoegde gerecht van de plaats waar de licentiegever is gevestigd of zijn voornaamste activiteit uitoefent.
15.Toepasselijk recht
Onverminderd specifieke overeenkomsten tussen de partijen,
— wordt deze licentie beheerst door het recht van de lidstaat van de Europese Unie waar de licentiegever zijn statutaire zetel, verblijfplaats of hoofdkantoor heeft,
— wordt deze licentie beheerst door het Belgische recht indien de licentiegever geen statutaire zetel, verblijfplaats of hoofdkantoor heeft in een lidstaat van de Europese Unie.
Aanhangsel
„Verenigbare licenties” in de zin van artikel 5 EUPL zijn:
— GNU General Public License (GPL) v. 2, v. 3
— GNU Affero General Public License (AGPL) v. 3
— Open Software License (OSL) v. 2.1, v. 3.0
— Eclipse Public License (EPL) v. 1.0
— CeCILL v. 2.0, v. 2.1
— Mozilla Public Licence (MPL) v. 2
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) voor andere werken dan software
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) of Strong Reciprocity (LiLiQ-R+).
De Europese Commissie kan dit aanhangsel actualiseren in geval van latere versies van de bovengenoemde licenties zonder dat er een nieuwe EUPL-versie wordt ontwikkeld, zolang die versies de uit hoofde van artikel 2 van deze licentie verleende rechten verlenen en ze de betrokken broncode beschermen tegen exclusieve toe-eigening.
Voor alle andere wijzigingen van of aanvullingen op dit aanhangsel is de ontwikkeling van een nieuwe EUPL-versie vereist.

View File

@@ -1,526 +0,0 @@
/**
* @file gate.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary A class to interact and manipulate machines with a non-euclidian curve
* @description A class to interact and manipulate machines with a non-euclidian curve
* @module ggc
* @exports ggc
* @version 2.0.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - rene@thegoldenbasket.nl
*
*/
//load local dependencies
const EventEmitter = require('events');
const Logger = require('../../../generalFunctions/helper/logger');
const { MeasurementContainer } = require('../../../generalFunctions/helper/measurements/index');
const Interpolation = require('../../../predict/dependencies/predict/interpolation');
//load all config modules
const defaultConfig = require('./ggcConfig.json');
const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
//load registration utility
const ChildRegistrationUtils = require('../../../generalFunctions/helper/childRegistrationUtils');
class Ggc {
/*------------------- Construct and set vars -------------------*/
constructor(ggcConfig = {}) {
//basic setup
this.emitter = new EventEmitter(); // Own EventEmitter
this.configUtils = new ConfigUtils(defaultConfig);
this.config = this.configUtils.initConfig(ggcConfig);
// Initialize measurements
this.measurements = new MeasurementContainer();
this.interpolation = new Interpolation();
this.child = {}; // object to hold child
this.actuators = []; // object to hold actuators
this.abortController = null; // new abort controller for aborting async tasks
// Init after config is set
this.logger = new Logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.name);
this.mode = this.config.mode.current;
this.move_delay = this.config.settings.moveDelay ; //define opening delay in seconds between 2 gates
this.state = "gateGroupClosed"; //define default starting state of the gates
//auto close
this.autoClose = true;
this.autoCloseTime = this.config.settings.autoClose;
this.autoCloseCnt = 0;
//protection sensor
this.safetySensor = false;
this.retryDelay = this.config.settings.retryDelay; // in seconds
this.closeAttempt = 0;
this.maxCloseAttempts = this.config.settings.maxRetries ;
this.safetySensorCnt = 0;
//ground loop trigger
this.ground_loop = false;
this.ground_loop_start = Date.now();
this.ground_loop_open = 10; //define time in seconds for when the ground loop should trigger a respons
//define if something has gone through the gate
this.goneThrough = false;
//define if the gate is closed
this.checkGateClosed = [false, false]; // gate 1 and gate 2
/* time controlled functions*/
//this.sleep = ms => new Promise(res => setTimeout(res, ms));
this.childRegistrationUtils = new ChildRegistrationUtils(this); // Child registration utility
}
isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
return allowedSourcesSet.has(source);
}
isValidActionForMode(action, mode) {
const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
return allowedActionsSet.has(action);
}
sleep(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
// only attach abort listener if a valid signal is provided
if (signal && typeof signal.addEventListener === 'function') {
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new Error('aborted'));
});
}
});
}
// -------- Sequence Handlers -------- //
async executeSequence(name) {
const sequence = this.config.sequences[name];
const positions = this.actuators.map(a => a.state.getCurrentPosition());
const states = this.actuators.map(a => a.state.getCurrentState());
if (!sequence || sequence.size === 0) {
this.logger.warn(`Sequence '${name}' not defined.`);
return;
}
// Abort any prior sequence and start fresh
this.abortController?.abort();
this.abortController = new AbortController();
const { signal } = this.abortController;
if ( states.some(s => s !== "operational") && name !== "stop2gates" ) {
this.logger.warn(`Actuators not operational, aborting sequence '${name}'.`);
this.handleInput("parent", "execSequence", "stop2gates");
this.sleep(1000).then(() => {
this.handleInput("parent", "execSequence", name);
});
return;
}
try {
for (const action of sequence) {
this.transitionToSequence(action);
//If someone has already called abort(), skip the delay
if (signal.aborted) {
continue;
}
//otherwise, wait for the delay
await this.sleep(this.move_delay * 1000, signal);
}
} catch (err) {
if (err.message === 'aborted') {
this.logger.debug(`Sequence '${name}' aborted mid-delay.`);
} else {
this.logger.error(`Error in sequence '${name}': ${err.stack}`);
}
} finally {
// Clean up so we know no sequence is running
this.abortController = null;
}
}
async transitionToSequence(action) {
this.logger.debug(`Executing action: ${action}`);
const positions = this.actuators.map(a => a.state.getCurrentPosition());
const states = this.actuators.map(a => a.state.getCurrentState());
// Perform actions based on the state
switch (action) {
case "openGate1":
this.logger.debug("Opening gate 1");
this.actuators[0].handleInput("parent", "execMovement", 100);
this.checkGateClosed[0] = false;
break;
case "openGate2":
this.logger.debug("Opening gate 2");
this.actuators[1].handleInput("parent", "execMovement", 100);
break;
case "stopGate1":
this.logger.debug("Stopping gate 1");
// abort the delayed sleep, if any
this.abortController?.abort();
// immediately stop actuator 1
this.actuators[0].stop();
break;
case "stopGate2":
this.logger.debug("Stopping gate 2");
// abort the delayed sleep, if any
this.abortController?.abort();
// immediately stop actuator 2
this.actuators[1].stop();
break;
case "closeGate1":
this.actuators[0].handleInput("parent", "execMovement", 0);
break;
case "closeGate2":
this.actuators[1].handleInput("parent", "execMovement", 0);
break;
default:
this.logger.warn(`Unknown state: ${state}`);
}
}
async handleInput(source, action, parameter) {
if (!this.isValidSourceForMode(source, this.mode)) {
this.logger.warn(`Invalid source ${source} for mode ${this.mode}`);
return;
}
if (!this.isValidActionForMode(action, this.mode)) {
this.logger.warn(`Invalid action ${action} for mode ${this.mode}`);
return;
}
switch (action) {
case 'execSequence':
this.executeSequence(parameter);
break;
case 'setMode':
this.setMode(parameter);
break;
default:
this.logger.warn(`Unknown action ${action}`);
}
}
groundLoopAction(){
if(this.ground_loop){
//keep track of time
this.ground_loop_time = Date.now() - this.ground_loop_trigger;
}
else{
this.ground_loop_time = 0;
}
if(this.ground_loop_time >= ( this.ground_loop_open * 1000) ){
this.openGates();
}
}
updateMeasurement(variant, subType, value, position) {
this.logger.debug(`---------------------- updating ${subType} ------------------ `);
switch (subType) {
case "power":
// Update power measurement
this.updatePower(variant, value, position);
break;
default:
this.logger.error(`Type '${type}' not recognized for measured update.`);
return;
}
}
updatePower(variant,value,position) {
switch (variant) {
case ("measured"):
// put value in measurements
this.measurements.type("power").variant(variant).position("wire").value(value);
this.eventUpdate();
this.logger.debug(`Measured: ${value}`);
break;
default:
this.logger.warn(`Unrecognized variant '${variant}' for update.`);
break;
}
}
eventUpdate() {
// Gather raw data in arrays
const positions = this.actuators.map(a => a.state.getCurrentPosition());
const states = this.actuators.map(a => a.state.getCurrentState());
this.logger.debug(`States: ${JSON.stringify(states)}`);
this.logger.debug(`Positions: ${JSON.stringify(positions)}`);
const totPower = this.measurements.type("power").variant("measured").position("wire").getCurrentValue();
// Utility flags
const allOperational = states.every(s => s === "operational");
const allAtOpen = positions.every(p => p === 100);
const allAtClosed = positions.every(p => p === 0);
const allAccelerating = states.every(s => s === "accelerating");
const allDecelerating = states.every(s => s === "decelerating");
const allStopped = states.every(s => s === "operational") && positions.every( p => p !== 0 && p != 100);
const onlyGateOneAccelerating = states[0] === "accelerating" && states[1] === "operational";
const onlyGateTwoAccelerating = states[1] === "accelerating" && states[0] === "operational";
const onlyGateOneDecelerating = states[0] === "decelerating" && states[1] === "operational";
const onlyGateTwoDecelerating = states[1] === "decelerating" && states[0] === "operational";
const oneOpenOneClosed = allOperational && positions.some(p => p === 0) && positions.some(p => p === 100);
// Threshold for “spike” detection (tune as needed)
const SPIKE_THRESHOLD_1gate = 50;
const SPIKE_THRESHOLD_2gates = 100;
const lowerPositionThreshold = 10; // 10% of the total range
const upperPositionThreshold = 90; // 90% of the total range
// When something is blocking the gate we need to reopen the gates (True means nothing is blocking)
if (!this.safetySensor) {
// always add 1 to the safety sensor counter
this.safetySensorCnt++;
//add 1 to the autoclose counter to check weither we dont exceedd the max retries
if(this.autoClose) {
this.autoCloseCnt++;
}
//check if the safety sensor is triggered and the gates are closing
if( allDecelerating || onlyGateOneDecelerating || onlyGateTwoDecelerating) {
this.closeAttempt++;
this.handleInput("parent", "execSequence", "stop2gates");
this.logger.debug("something is blocking the gate, stopping actuators");
this.sleep(1000).then(() => {
this.handleInput("parent", "execSequence", "open2gates");
});
}
}
// Detect if any single gate is decelerating into its stop
if( onlyGateOneDecelerating ) {
//check for power spike so we know the gate is closed
if ( totPower > SPIKE_THRESHOLD_1gate ) {
this.logger.debug("Gate 1 is decelerating into the stop (power spike)");
//check flag for knowing if the gate is closed
this.checkGateClosed[0] = true;
this.closeAttempt = 0;
}
}
if( allDecelerating || allAccelerating) {
if( totPower > SPIKE_THRESHOLD_2gates && ( positions.some(p => p > lowerPositionThreshold) || positions.some(p => p < upperPositionThreshold) ) ) {
this.logger.debug("Unexpected power spike detected");
// stop the actuators
this.handleInput("parent", "execSequence", "stop2gates");
}
}
// Decide group state
if (allAtOpen && allOperational) {
this.state = "gateGroupOpened";
//trigger auto close if count is smaller than max
if( this.autoClose && this.autoCloseCnt < this.maxCloseAttempts && this.safetySensorCnt > 0) {
this.sleep(this.autoCloseTime * 1000).then(() => {
this.handleInput("parent", "execSequence", "close2gates");
//reset the safetySensor count because we are automatically closing the gates and if its bigger than 0 it means some1 passed through it
this.safetySensorCnt = 0;
});
}
this.logger.debug("Gates are open");
}
else if (allAtClosed && allOperational) {
this.state = "gateGroupClosed";
//after everything was closed and the auto close is enabled we need to reset the auto close count
if(this.autoClose) {
this.autoCloseCnt = 0;
};
this.logger.debug("Gates are closed");
}
else if (oneOpenOneClosed) {
this.state = "oneGateOpenOneGateClosed";
this.logger.debug("One gate open, one gate closed");
}
else if (allAccelerating) {
this.state = "gateGroupAccelerating";
this.logger.debug("Gates are accelerating");
}
else if (onlyGateOneAccelerating) {
this.state = "gateOneAccelerating";
this.logger.debug("Only gate 1 is accelerating");
}
else if (onlyGateTwoAccelerating) {
this.state = "gateTwoAccelerating";
this.logger.debug("Only gate 2 is accelerating");
}
else if (allDecelerating) {
this.state = "gateGroupDecelerating";
this.logger.debug("Gates are decelerating");
}
else if (onlyGateOneDecelerating) {
this.state = "gateOneDecelerating";
this.logger.debug("Only gate 1 is decelerating");
}
else if (onlyGateTwoDecelerating) {
this.state = "gateTwoDecelerating";
this.logger.debug("Only gate 2 is decelerating");
}
else if (allStopped) {
this.state = "gateGroupStopped";
this.logger.debug("Gates are stopped");
}
else {
this.state = "unknown";
this.logger.warn(`Unhandled combination: positions=${positions}, states=${states}`);
}
// if the gates are operational and close but we dont see the truely closed state then we need to nudge the gate to force the close
}
getOutput() {
// Improved output object generation
const output = {};
//build the output object
this.measurements.getTypes().forEach(type => {
this.measurements.getVariants(type).forEach(variant => {
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
if (downstreamVal != null) {
output[`downstream_${variant}_${type}`] = downstreamVal;
}
if (upstreamVal != null) {
output[`upstream_${variant}_${type}`] = upstreamVal;
}
if (downstreamVal != null && upstreamVal != null) {
const diffVal = this.measurements.type(type).variant(variant).difference().value;
output[`differential_${variant}_${type}`] = diffVal;
}
});
});
//fill in the rest of the output object
output["mode"] = this.mode;
output["totPower"] = this.power;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
return output;
}
} // end of class
module.exports = Ggc;
/*
const ggcConfig = {
general: {
name: "TestGGC",
logging: {
enabled: true,
logLevel: "debug"
}
},
settings: {
moveDelay: 3,
autoClose: 5,
retryDelay: 10,
maxRetries: 5
}
};
const ggc = new Ggc(ggcConfig);
const linearActuator = require('../../../linearActuator/dependencies/linearActuator/linearActuator');
const linActConfig =
{
general: {
logging: {
enabled: true,
logLevel: "debug",
}
},
settings: {
moveDelay: 3,
autoClose: 5,
retryDelay: 10,
maxRetries: 5
}
};
const stateConfig = {
general: {
logging: {
enabled: true,
logLevel: "debug"
}
},
movement: {
speed: 0.1,
mode: "staticspeed"
},
time: {
starting: 0,
warmingup: 0,
stopping: 0,
coolingdown: 0
}
};
const gate1 = new linearActuator(linActConfig,stateConfig);
const gate2 = new linearActuator(linActConfig,stateConfig);
ggc.childRegistrationUtils.registerChild(gate1,"upstream");
ggc.childRegistrationUtils.registerChild(gate2,"downstream");
//open completely 2 gates inside an async IIFE
(async () => {
await ggc.actuators[0].handleInput("parent","execSequence","startup");
await ggc.actuators[1].handleInput("parent","execSequence","startup");
ggc.handleInput("parent","execSequence","open2gates");
await ggc.sleep(5000);
ggc.handleInput("parent","execSequence","stop2gates");
})();
//*/

View File

@@ -1,297 +0,0 @@
{
"general": {
"name": {
"default": "gate group control Machine",
"rules": {
"type": "string",
"description": "A human-readable name or label for this machine configuration."
}
},
"id": {
"default": null,
"rules": {
"type": "string",
"nullable": true,
"description": "A unique identifier for this configuration. If not provided, defaults to null."
}
},
"unit": {
"default": "m3/h",
"rules": {
"type": "string",
"description": "The default measurement unit for this configuration (e.g., 'meters', 'seconds', 'unitless')."
}
},
"logging": {
"logLevel": {
"default": "info",
"rules": {
"type": "enum",
"values": [
{
"value": "debug",
"description": "Log messages are printed for debugging purposes."
},
{
"value": "info",
"description": "Informational messages are printed."
},
{
"value": "warn",
"description": "Warning messages are printed."
},
{
"value": "error",
"description": "Error messages are printed."
}
]
}
},
"enabled": {
"default": true,
"rules": {
"type": "boolean",
"description": "Indicates whether logging is active. If true, log messages will be generated."
}
}
}
},
"functionality": {
"softwareType": {
"default": "gateGroupControl",
"rules": {
"type": "string",
"description": "Specified software type for this configuration."
}
},
"role": {
"default": "gate controller",
"rules": {
"type": "string",
"description": "Indicates the role this configuration plays within the system."
}
}
},
"asset": {
"uuid": {
"default": null,
"rules": {
"type": "string",
"nullable": true,
"description": "A universally unique identifier for this asset. May be null if not assigned."
}
},
"geoLocation": {
"default": {},
"rules": {
"type": "object",
"description": "An object representing the asset's physical coordinates or location.",
"schema": {
"x": {
"default": 0,
"rules": {
"type": "number",
"description": "X coordinate of the asset's location."
}
},
"y": {
"default": 0,
"rules": {
"type": "number",
"description": "Y coordinate of the asset's location."
}
},
"z": {
"default": 0,
"rules": {
"type": "number",
"description": "Z coordinate of the asset's location."
}
}
}
}
}
},
"mode": {
"current": {
"default": "auto",
"rules": {
"type": "enum",
"values": [
{
"value": "auto",
"description": "Machine accepts setpoints from a parent controller and runs autonomously."
},
{
"value": "virtualControl",
"description": "Controlled via GUI setpoints; ignores parent commands."
},
{
"value": "fysicalControl",
"description": "Controlled via physical buttons or switches; ignores external automated commands."
},
{
"value": "maintenance",
"description": "No active control from auto, virtual, or fysical sources."
}
],
"description": "The operational mode of the machine."
}
},
"allowedActions":{
"default":{},
"rules": {
"type": "object",
"schema":{
"auto": {
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in auto mode."
}
},
"virtualControl": {
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in virtualControl mode."
}
},
"fysicalControl": {
"default": ["statusCheck", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in fysicalControl mode."
}
},
"maintenance": {
"default": ["statusCheck"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in maintenance mode."
}
}
},
"description": "Information about valid command sources recognized by the machine."
}
},
"allowedSources":{
"default": {},
"rules": {
"type": "object",
"schema":{
"auto": {
"default": ["parent", "GUI", "fysical"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Sources allowed in auto mode."
}
},
"virtualControl": {
"default": ["GUI", "fysical"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Sources allowed in virtualControl mode."
}
},
"fysicalControl": {
"default": ["fysical"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Sources allowed in fysicalControl mode."
}
}
},
"description": "Information about valid command sources recognized by the machine."
}
}
},
"sequences":{
"default":{},
"rules": {
"type": "object",
"schema": {
"open2gates": {
"default": ["openGate1","openGate2"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Sequence of states for starting up the machine."
}
},
"open1gate": {
"default": ["openGate1"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Sequence of states for shutting down the machine."
}
},
"stop2gates": {
"default": ["stopGate1","stopGate2"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Sequence of states for stopping the machine."
}
},
"close2gates": {
"default": ["closeGate2","closeGate1"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Sequence of states for closing the gates."
}
}
}
},
"description": "Predefined sequences of states for the machine."
},
"settings": {
"moveDelay": {
"default": 3,
"rules": {
"type": "number",
"description": "delay between opening first and second linear actuator in seconds"
}
},
"autoClose": {
"default": 30,
"rules": {
"type": "number",
"description": "When auto close is enabled, the gate will close automatically after this time in seconds"
}
},
"retryDelay": {
"default": 5,
"rules": {
"type": "number",
"description": "Delay in seconds before retrying a failed command."
}
},
"maxRetries": {
"default": 5,
"rules": {
"type": "number",
"description": "Maximum number of retries for a failed command."
}
},
"groundLoopOpen": {
"default": 10,
"rules": {
"type": "number",
"description": "Time before ground loop triggers opening of the gate in seconds"
}
}
}
}

View File

@@ -1,566 +0,0 @@
const MachineGroup = require('./machineGroup');
const Machine = require('../../../rotatingMachine/dependencies/machine/machine');
const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json');
class MachineGroupTester {
constructor() {
this.totalTests = 0;
this.passedTests = 0;
this.failedTests = 0;
this.machineCurve = specs[0].machineCurve;
}
assert(condition, message) {
this.totalTests++;
if (condition) {
console.log(`✓ PASS: ${message}`);
this.passedTests++;
} else {
console.log(`✗ FAIL: ${message}`);
this.failedTests++;
}
}
createBaseMachineConfig(name) {
return {
general: {
logging: { enabled: true, logLevel: "debug" },
name: name,
unit: "m3/h"
},
functionality: {
softwareType: "machine",
role: "RotationalDeviceController"
},
asset: {
type: "pump",
subType: "Centrifugal",
model: "TestModel",
supplier: "Hydrostal",
machineCurve: this.machineCurve
},
mode: {
current: "auto",
allowedActions: {
auto: ["execSequence", "execMovement", "statusCheck"],
virtualControl: ["execMovement", "statusCheck"],
fysicalControl: ["statusCheck"]
},
allowedSources: {
auto: ["parent", "GUI"],
virtualControl: ["GUI"],
fysicalControl: ["fysical"]
}
},
sequences: {
startup: ["starting", "warmingup", "operational"],
shutdown: ["stopping", "coolingdown", "idle"],
emergencystop: ["emergencystop", "off"],
boot: ["idle", "starting", "warmingup", "operational"]
},
calculationMode: "medium"
};
}
createBaseMachineGroupConfig(name) {
return {
general: {
logging: { enabled: true, logLevel: "debug" },
name: name
},
functionality: {
softwareType: "machineGroup",
role: "GroupController"
},
scaling: {
current: "normalized"
},
mode: {
current: "optimalControl"
}
};
}
async testSingleMachineOperation() {
console.log('\nTesting Single Machine Operation...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
const machineConfig = this.createBaseMachineConfig("TestMachine1");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(machineConfig);
// Register machine with group
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
// Test 1: Basic initialization
this.assert(
Object.keys(mg.machines).length === 0,
'Machine group should have exactly zero machine'
);
// Test 2: Calculate demand with single machine
await machine.handleInput("parent", "execSequence", "startup");
await mg.handleFlowInput(50);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Total flow should be greater than 0 for demand of 50'
);
// Test 3: Check machine mode handling
machine.setMode("virtualControl");
const {single, machineNum} = mg.singleMachine();
this.assert(
single === true,
'Should identify as single machine when in virtual control'
);
// Test 4: Zero demand handling
await mg.handleFlowInput(0);
this.assert(
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
'Total flow should be 0 for zero demand'
);
// Test 5: Max demand handling
await mg.handleFlowInput(100);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Total flow should be greater than 0 for max demand'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testMultipleMachineOperation() {
console.log('\nTesting Multiple Machine Operation...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine1 = new Machine(this.createBaseMachineConfig("Machine1"));
const machine2 = new Machine(this.createBaseMachineConfig("Machine2"));
mg.childRegistrationUtils.registerChild(machine1, "downstream");
mg.childRegistrationUtils.registerChild(machine2, "downstream");
machine1.measurements.type("pressure").variant("measured").position("downstream").value(800);
machine2.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine1.state.transitionToState("idle");
await machine2.state.transitionToState("idle");
await machine1.handleInput("parent", "execSequence", "startup");
await machine2.handleInput("parent", "execSequence", "startup");
// Test 1: Multiple machine registration
this.assert(
Object.keys(mg.machines).length === 2,
'Machine group should have exactly two machines'
);
// Test 1.1: Calculate demand with multiple machines
await mg.handleFlowInput(0); // Testing with higher demand for two machines
const machineOutputs = Object.keys(mg.machines).filter(id =>
mg.machines[id].measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0
);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0 &&
machineOutputs.length > 0,
'Should distribute load between machines'
);
// Test 1.2: Calculate demand with multiple machines with an increment of 10
for(let i = 0; i < 100; i+=10){
await mg.handleFlowInput(i); // Testing with incrementing demand
const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
this.assert(
flowValue !== undefined && !isNaN(flowValue),
`Should handle demand of ${i} units properly`
);
}
// Test 2: Calculate nonsense demands with multiple machines
await mg.handleFlowInput(150); // Testing with higher demand for two machines
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Should handle excessive demand gracefully'
);
// Test 3: Force single machine mode
machine2.setMode("maintenance");
const {single} = mg.singleMachine();
this.assert(
single === true,
'Should identify as single machine when one machine is in maintenance'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testDynamicTotals() {
console.log('\nTesting Dynamic Totals...');
const mg = new MachineGroup(this.createBaseMachineGroupConfig("TestMachineGroup"));
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
try {
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Test 1: Dynamic totals initialization
const maxFlow = machine.predictFlow.currentFxyYMax;
const maxPower = machine.predictPower.currentFxyYMax;
this.assert(
mg.dynamicTotals.flow.max === maxFlow && mg.dynamicTotals.power.max === maxPower,
'Dynamic totals should reflect machine capabilities'
);
// Test 2: Demand scaling
await mg.handleFlowInput(50); // 50% of max
const actualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
this.assert(
actualFlow <= maxFlow * 0.6, // Allow some margin for interpolation
'Scaled demand should be approximately 50% of max flow'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testInterpolation() {
console.log('\nTesting Interpolation...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
const machineConfig = this.createBaseMachineConfig("TestMachine");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(machineConfig);
// Register machine and set initial state
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(1);
machine.state.transitionToState("idle");
// Test interpolation at different demand points
const testPoints = [0, 25, 50, 75, 100];
for (const demand of testPoints) {
await mg.handleFlowInput(demand);
const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
const powerValue = mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue();
this.assert(
flowValue !== undefined && !isNaN(flowValue),
`Interpolation should produce valid flow value for demand ${demand}`
);
this.assert(
powerValue !== undefined && !isNaN(powerValue),
`Interpolation should produce valid power value for demand ${demand}`
);
}
// Test interpolation between curve points
const interpolatedPoint = 45; // Should interpolate between 40 and 60
await mg.handleFlowInput(interpolatedPoint);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
`Interpolation should handle non-exact point ${interpolatedPoint}`
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testSingleMachineControlModes() {
console.log('\nTesting Single Machine Control Modes...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
const machineConfig = this.createBaseMachineConfig("TestMachine1");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(machineConfig);
// Register machine and initialize
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Test 1: Virtual Control Mode
machine.setMode("virtualControl");
await mg.handleFlowInput(50);
this.assert(
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined,
'Should handle virtual control mode'
);
// Test 2: Physical Control Mode
machine.setMode("fysicalControl");
await mg.handleFlowInput(75);
this.assert(
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined,
'Should handle physical control mode'
);
// Test 3: Auto Mode Return
machine.setMode("auto");
await mg.handleFlowInput(60);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Should return to normal operation in auto mode'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testMachinesOffNormalized() {
console.log('\nTesting Machines Off with Normalized Flow...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffNormalized");
// scaling is "normalized" by default
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffNormalized"));
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Turn machines off by setting demand to 0 with normalized scaling
await mg.handleFlowInput(-1);
this.assert(
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
'Total flow should be 0 when demand is < 0 in normalized scaling'
);
}
async testMachinesOffAbsolute() {
console.log('\nTesting Machines Off with Absolute Flow...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffAbsolute");
// Switch scaling to "absolute"
machineGroupConfig.scaling.current = "absolute";
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffAbsolute"));
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Turn machines off by setting demand to 0 with absolute scaling
await mg.handleFlowInput(0);
this.assert(
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
'Total flow should be 0 when demand is 0 in absolute scaling'
);
}
async testPriorityControl() {
console.log('\nTesting Priority Control...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_Priority");
const mg = new MachineGroup(machineGroupConfig);
try {
// Create 3 machines with different configurations for clearer testing
const machines = [];
for(let i = 1; i <= 3; i++) {
const machineConfig = this.createBaseMachineConfig(`Machine${i}`);
const machine = new Machine(machineConfig);
machines.push(machine);
mg.childRegistrationUtils.registerChild(machine, "downstream");
// Set different max flows to make priority visible
machine.predictFlow = {
currentFxyYMin: 10 * i, // Different min flows
currentFxyYMax: 50 * i // Different max flows
};
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Mock the inputFlowCalcPower method for testing
machine.inputFlowCalcPower = (flow) => flow * 2; // Simple mock function
}
// Test 1: Default priority (by machine ID)
// Use handleInput which routes to equalControl in prioritycontrol mode
await mg.handleInput("parent", 80);
const flowAfterDefaultPriority = Object.values(mg.machines).map(machine =>
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0
);
this.assert(
flowAfterDefaultPriority[0] > 0 && flowAfterDefaultPriority[1] > 0 && flowAfterDefaultPriority[2] === 0,
'Default priority should use machines in ID order until demand is met'
);
// Test 2: Custom priority list
await mg.handleInput("parent", 120, Infinity, [3, 2, 1]);
await new Promise(resolve => setTimeout(resolve, 100));
const flowAfterCustomPriority = Object.values(mg.machines).map(machine =>
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0
);
this.assert(
flowAfterCustomPriority[2] > 0 && flowAfterCustomPriority[1] > 0 && flowAfterCustomPriority[0] === 0,
'Custom priority should use machines in specified order until demand is met'
);
// Test 3: Zero demand should shut down all machines
await mg.handleInput("parent", 0);
const noFlowCondition = Object.values(mg.machines).every(machine =>
!machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0
);
this.assert(
noFlowCondition,
'Zero demand should result in no flow from any machine'
);
// Test 4: Handling excessive demand (more than total capacity)
const totalMaxFlow = machines.reduce((sum, machine) => sum + machine.predictFlow.currentFxyYMax, 0);
await mg.handleInput("parent", totalMaxFlow + 100);
const totalActualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
this.assert(
totalActualFlow <= totalMaxFlow && totalActualFlow > 0,
'Excessive demand should be capped to maximum possible flow'
);
// Test 5: Check all measurements are updated correctly
this.assert(
mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() > 0 &&
mg.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue() > 0,
'All measurements should be updated after priority control'
);
} catch (error) {
console.error('Priority control test failed with error:', error);
this.failedTests++;
}
}
async runAllTests() {
console.log('Starting MachineGroup Tests...\n');
await this.testSingleMachineOperation();
await this.testMultipleMachineOperation();
await this.testDynamicTotals();
await this.testInterpolation();
await this.testSingleMachineControlModes();
await this.testMachinesOffNormalized();
await this.testMachinesOffAbsolute();
await this.testPriorityControl(); // Add the new test
await testCombinationIterations();
console.log('\nTest Summary:');
console.log(`Total Tests: ${this.totalTests}`);
console.log(`Passed: ${this.passedTests}`);
console.log(`Failed: ${this.failedTests}`);
// Return exit code based on test results
process.exit(this.failedTests > 0 ? 1 : 0);
}
}
// Add a custom logger to capture debug logs during tests
class CapturingLogger {
constructor() {
this.logs = [];
}
debug(message) {
this.logs.push({ level: "debug", message });
console.debug(message);
}
info(message) {
this.logs.push({ level: "info", message });
console.info(message);
}
warn(message) {
this.logs.push({ level: "warn", message });
console.warn(message);
}
error(message) {
this.logs.push({ level: "error", message });
console.error(message);
}
getAll() {
return this.logs;
}
clear() {
this.logs = [];
}
}
// Modify one of the test functions to override the machineGroup logger
async function testCombinationIterations() {
console.log('\nTesting Combination Iterations Logging...');
const machineGroupConfig = tester.createBaseMachineGroupConfig("TestCombinationIterations");
const mg = new MachineGroup(machineGroupConfig);
// Override logger with a capturing logger
const customLogger = new CapturingLogger();
mg.logger = customLogger;
// Create one machine for simplicity (or two if you like)
const machine = new Machine(tester.createBaseMachineConfig("TestMachineForCombo"));
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// For testing, force dynamic totals so that combination search is exercised
mg.dynamicTotals.flow = { min: 0, max: 200 }; // example totalling
// Call handleFlowInput with a demand that requires iterations
await mg.handleFlowInput(120);
// After running, output captured iteration debug logs
console.log("\n-- Captured Debug Logs for Combination Search Iterations --");
customLogger.getAll().forEach(log => {
if(log.level === "debug") {
console.log(log.message);
}
});
// Also output best result details if any needed for further improvement
console.log("\n-- Final Output --");
const totalFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
console.log("Total Flow: ", totalFlow);
// Get machine outputs by checking each machine's measurements
const machineOutputs = {};
Object.entries(mg.machines).forEach(([id, machine]) => {
const flow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
if (flow) machineOutputs[id] = flow;
});
console.log("Machine Outputs: ", machineOutputs);
}
// Run the tests
const tester = new MachineGroupTester();
tester.runAllTests().catch(console.error);

View File

@@ -1,188 +0,0 @@
{
"general": {
"name": {
"default": "Machine Group Configuration",
"rules": {
"type": "string",
"description": "A human-readable name or label for this machine group configuration."
}
},
"id": {
"default": null,
"rules": {
"type": "string",
"nullable": true,
"description": "A unique identifier for this configuration. If not provided, defaults to null."
}
},
"logging": {
"logLevel": {
"default": "info",
"rules": {
"type": "enum",
"values": [
{
"value": "debug",
"description": "Log messages are printed for debugging purposes."
},
{
"value": "info",
"description": "Informational messages are printed."
},
{
"value": "warn",
"description": "Warning messages are printed."
},
{
"value": "error",
"description": "Error messages are printed."
}
]
}
},
"enabled": {
"default": true,
"rules": {
"type": "boolean",
"description": "Indicates whether logging is active. If true, log messages will be generated."
}
}
}
},
"functionality": {
"softwareType": {
"default": "machineGroup",
"rules": {
"type": "string",
"description": "Logical name identifying the software type."
}
},
"role": {
"default": "GroupController",
"rules": {
"type": "string",
"description": "Controls a group of machines within the system."
}
}
},
"mode": {
"current": {
"default": "optimalControl",
"rules": {
"type": "enum",
"values": [
{
"value": "optimalControl",
"description": "The group controller selects the most optimal combination of machines based on their real-time performance curves."
},
{
"value": "priorityControl",
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added."
},
{
"value": "prioritypercentagecontrol",
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added based on a percentage of the total demand."
},
{
"value": "maintenance",
"description": "The group is in maintenance mode with limited actions (monitoring only)."
}
],
"description": "The operational mode of the machine group controller."
}
},
"allowedActions": {
"default": {},
"rules": {
"type": "object",
"schema": {
"optimalControl": {
"default": ["statusCheck", "execOptimalCombination", "balanceLoad", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in optimalControl mode."
}
},
"priorityControl": {
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in priorityControl mode."
}
},
"prioritypercentagecontrol": {
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in manualOverride mode."
}
},
"maintenance": {
"default": ["statusCheck"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in maintenance mode."
}
}
},
"description": "Defines the actions available for each operational mode of the machine group controller."
}
},
"allowedSources": {
"default": {},
"rules": {
"type": "object",
"schema": {
"optimalcontrol": {
"default": ["parent", "GUI", "physical", "API"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Command sources allowed in optimalControl mode."
}
},
"prioritycontrol": {
"default": ["parent", "GUI", "physical", "API"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Command sources allowed in priorityControl mode."
}
},
"prioritypercentagecontrol": {
"default": ["parent", "GUI", "physical", "API"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Command sources allowed "
}
}
},
"description": "Specifies the valid command sources recognized by the machine group controller for each mode."
}
}
},
"scaling": {
"current": {
"default": "normalized",
"rules": {
"type": "enum",
"values": [
{
"value": "normalized",
"description": "Scales the demand between 0100% of the total flow capacity, interpolating to calculate the effective demand."
},
{
"value": "absolute",
"description": "Uses the absolute demand value directly, capped between the min and max machine flow capacities."
}
],
"description": "The scaling mode for demand calculations."
}
}
}
}

137
dependencies/test.js vendored
View File

@@ -1,137 +0,0 @@
/**
* This file implements a pump optimization algorithm that:
* 1. Models different pumps with efficiency characteristics
* 2. Determines all possible pump combinations that can meet a demand flow
* 3. Finds the optimal combination that minimizes power consumption
* 4. Tests the algorithm with different demand levels
*/
/**
* Pump Class
* Represents a pump with specific operating characteristics including:
* - Maximum flow capacity
* - Center of Gravity (CoG) for efficiency
* - Efficiency curve mapping flow percentages to power consumption
*/
class Pump {
constructor(name, maxFlow, cog, efficiencyCurve) {
this.name = name;
this.maxFlow = maxFlow; // Maximum flow at a given pressure
this.CoG = cog; // Efficiency center of gravity percentage
this.efficiencyCurve = efficiencyCurve; // Flow % -> Power usage mapping
}
/**
* Returns pump flow at a given pressure
* Currently assumes constant flow regardless of pressure
*/
getFlow(pressure) {
return this.maxFlow; // Assume constant flow at a given pressure
}
/**
* Calculates power consumption based on flow and pressure
* Uses the efficiency curve when available, otherwise uses linear approximation
*/
getPowerConsumption(flow, pressure) {
let flowPercent = flow / this.maxFlow;
return this.efficiencyCurve[flowPercent] || (1.2 * flow); // Default linear approximation
}
}
/**
* Test pump definitions
* Three pump models with different flow capacities and efficiency characteristics
*/
const pumps = [
new Pump("Pump A", 100, 0.6, {0.6: 50, 0.8: 70, 1.0: 100}),
new Pump("Pump B", 120, 0.7, {0.6: 55, 0.8: 75, 1.0: 110}),
new Pump("Pump C", 90, 0.5, {0.5: 40, 0.7: 60, 1.0: 90}),
];
const pressure = 1.0; // Assume constant pressure
/**
* Get all valid pump combinations that meet the required demand flow (Qd)
*
* @param {Array} pumps - Available pump array
* @param {Number} Qd - Required demand flow
* @param {Number} pressure - System pressure
* @returns {Array} Array of valid pump combinations that can meet or exceed the demand
*
* This function:
* 1. Generates all possible subsets of pumps (power set)
* 2. Filters for non-empty subsets that can meet or exceed demand flow
*/
function getValidPumpCombinations(pumps, Qd, pressure) {
let subsets = [[]];
for (let pump of pumps) {
let newSubsets = subsets.map(set => [...set, pump]);
subsets = subsets.concat(newSubsets);
}
return subsets.filter(subset => subset.length > 0 &&
subset.reduce((sum, p) => sum + p.getFlow(pressure), 0) >= Qd);
}
/**
* Find the optimal pump combination that minimizes power consumption
*
* @param {Array} pumps - Available pump array
* @param {Number} Qd - Required demand flow
* @param {Number} pressure - System pressure
* @returns {Object} Object containing the best pump combination and its power consumption
*
* This function:
* 1. Gets all valid pump combinations that meet demand
* 2. For each combination, distributes flow based on CoG proportions
* 3. Calculates total power consumption for each distribution
* 4. Returns the combination with minimum power consumption
*/
function optimizePumpSelection(pumps, Qd, pressure) {
let validCombinations = getValidPumpCombinations(pumps, Qd, pressure);
let bestCombination = null;
let minPower = Infinity;
validCombinations.forEach(combination => {
let totalFlow = combination.reduce((sum, pump) => sum + pump.getFlow(pressure), 0);
let totalCoG = combination.reduce((sum, pump) => sum + pump.CoG, 0);
// Distribute flow based on CoG proportions
let flowDistribution = combination.map(pump => ({
pump,
flow: (pump.CoG / totalCoG) * Qd
}));
let totalPower = flowDistribution.reduce((sum, { pump, flow }) =>
sum + pump.getPowerConsumption(flow, pressure), 0);
if (totalPower < minPower) {
minPower = totalPower;
bestCombination = flowDistribution;
}
});
return { bestCombination, minPower };
}
/**
* Test function that runs optimization for different demand levels
* Tests from 0% to 100% of total available flow in 10% increments
* Outputs the selected pumps, flow allocation, and power consumption for each scenario
*/
console.log("Pump Optimization Results:");
const totalAvailableFlow = pumps.reduce((sum, pump) => sum + pump.getFlow(pressure), 0);
for (let i = 0; i <= 10; i++) {
let Qd = (i / 10) * totalAvailableFlow; // Incremental flow demand
let { bestCombination, minPower } = optimizePumpSelection(pumps, Qd, pressure);
console.log(`\nTotal Demand Flow: ${Qd.toFixed(2)}`);
console.log("Selected Pumps and Allocated Flow:");
bestCombination.forEach(({ pump, flow }) => {
console.log(` ${pump.name}: ${flow.toFixed(2)} units`);
});
console.log(`Total Power Consumption: ${minPower.toFixed(2)} kW`);
}

284
ggc.html
View File

@@ -1,284 +0,0 @@
<script type="module">
import * as menuUtils from "/generalFunctions/helper/menuUtils.js";
RED.nodes.registerType("ggc", {
category: "digital twin",
color: "#4f8582",
defaults: {
// Define default properties
name: { value: "", required: true },
enableLog: { value: false },
logLevel: { value: "error" },
// Define specific properties
speed: { value: 1, required: true },
startup: { value: 0 },
warmup: { value: 0 },
shutdown: { value: 0 },
cooldown: { value: 0 },
//define general asset properties
supplier: { value: "" },
subType: { value: "" },
model: { value: "" },
unit: { value: "" },
},
inputs: 1,
outputs: 4,
inputLabels: ["Input"],
outputLabels: ["process", "dbase", "upstreamParent", "downstreamParent"],
icon: "font-awesome/fa-cogs",
label: function () {
return this.name || "GateGroupControl";
},
oneditprepare: function () {
const node = this;
console.log("ggc Node: Edit Prepare");
const elements = {
// Basic fields
name: document.getElementById("node-input-name"),
// specific fields
speed: document.getElementById("node-input-speed"),
startup: document.getElementById("node-input-startup"),
warmup: document.getElementById("node-input-warmup"),
shutdown: document.getElementById("node-input-shutdown"),
cooldown: document.getElementById("node-input-cooldown"),
// Logging fields
logCheckbox: document.getElementById("node-input-enableLog"),
logLevelSelect: document.getElementById("node-input-logLevel"),
rowLogLevel: document.getElementById("row-logLevel"),
// Asset fields
supplier: document.getElementById("node-input-supplier"),
subType: document.getElementById("node-input-subType"),
model: document.getElementById("node-input-model"),
unit: document.getElementById("node-input-unit"),
};
const projecSettingstURL = "http://localhost:1880/generalFunctions/settings/projectSettings.json";
try{
// Fetch project settings
menuUtils.fetchProjectData(projecSettingstURL)
.then((projectSettings) => {
//assign to node vars
node.configUrls = projectSettings.configUrls;
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("ggc",node.configUrls.cloud.taggcodeAPI);
node.configUrls.cloud.config = cloudConfigURL; // first call
node.configUrls.local.config = localConfigURL; // backup call
node.locationId = projectSettings.locationId;
node.uuid = projectSettings.uuid;
// Gets the ID of the active workspace (Flow)
const activeFlowId = RED.workspaces.active(); //fetches active flow id
node.processId = activeFlowId;
// UI elements
menuUtils.initBasicToggles(elements);
menuUtils.fetchAndPopulateDropdowns(node.configUrls, elements, node); // function for all assets
})
}catch(e){
console.log("Error fetching project settings", e);
}
if(node.d){
//this means node is disabled
console.log("Current status of node is disabled");
}
},
oneditsave: function () {
const node = this;
console.log(`------------ Saving changes to node ------------`);
console.log(`${node.uuid}`);
//save basic properties
["name", "unit", "supplier", "subType", "model"].forEach(
(field) => {
const element = document.getElementById(`node-input-${field}`);
if (element) {
node[field] = element.value || "";
}
}
);
// Save numeric and boolean properties
["speed", "startup", "warmup", "shutdown", "cooldown"].forEach(
(field) => {
const element = document.getElementById(`node-input-${field}`);
if (element) {
node[field] = Number(element.value) || 0;
}
}
);
/*
//local db
node[field] = node["modelMetadata"][field];
//central db
node[field] = node["modelMetadata"]["product_model_meta"][field];
*/
const logLevelElement = document.getElementById("node-input-logLevel");
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
if (!node.unit) {
RED.notify("Unit selection is required.", "error");
}
if (node.subType && !node.unit) {
RED.notify("Unit must be set when specifying a subtype.", "error");
}
try{
console.log("Saving assetID and tagnumber");
console.log(node.assetTagCode);
// Fetch project settings
menuUtils.apiCall(node,node.configUrls)
.then((response) => {
console.log(" ====<<>>>> API call response", response);
//save response to node information
node.assetId = response.asset_id;
node.assetTagCode = response.asset_tag_number;
})
.catch((error) => {
console.log("Error during API call", error);
});
}catch(e){
console.log("Error saving assetID and tagnumber", e);
}
},
});
</script>
<!-- Main UI Template -->
<script type="text/html" data-template-name="ggc">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input
type="text"
id="node-input-name"
placeholder="ggc Name"
style="width:70%;"
/>
</div>
<div class="form-row">
<label for="node-input-speed"><i class="fa fa-clock-o"></i> Reaction Speed</label>
<input type="number" id="node-input-speed" placeholder="1" />
</div>
<div class="form-row">
<label for="node-input-startup"><i class="fa fa-clock-o"></i> Startup Time</label>
<input type="number" id="node-input-startup" placeholder="0" />
</div>
<div class="form-row">
<label for="node-input-warmup"><i class="fa fa-clock-o"></i> Warmup Time</label>
<input type="number" id="node-input-warmup" placeholder="0" />
</div>
<div class="form-row">
<label for="node-input-shutdown"><i class="fa fa-clock-o"></i> Shutdown Time</label>
<input type="number" id="node-input-shutdown" placeholder="0" />
</div>
<div class="form-row">
<label for="node-input-cooldown"><i class="fa fa-clock-o"></i> Cooldown Time</label>
<input type="number" id="node-input-cooldown" placeholder="0" />
</div>
<!-- Optional Extended Fields: supplier, type, subType, model -->
<hr />
<div class="form-row">
<label for="node-input-supplier"
><i class="fa fa-industry"></i> Supplier</label
>
<select id="node-input-supplier" style="width:60%;">
<option value="">(optional)</option>
</select>
</div>
<div class="form-row">
<label for="node-input-subType"
><i class="fa fa-puzzle-piece"></i> SubType</label
>
<select id="node-input-subType" style="width:60%;">
<option value="">(optional)</option>
</select>
</div>
<div class="form-row">
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
<select id="node-input-model" style="width:60%;">
<option value="">(optional)</option>
</select>
</div>
<div class="form-row">
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
<select id="node-input-unit" style="width:60%;"></select>
</div>
<hr />
<!-- loglevel checkbox -->
<div class="form-row">
<label for="node-input-enableLog"
><i class="fa fa-cog"></i> Enable Log</label
>
<input
type="checkbox"
id="node-input-enableLog"
style="width:20px; vertical-align:baseline;"
/>
<span>Enable logging</span>
</div>
<div class="form-row" id="row-logLevel">
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
<select id="node-input-logLevel" style="width:60%;">
<option value="info">Info</option>
<option value="debug">Debug</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
</script>
<script type="text/html" data-help-name="ggc">
<p>
<b>ggc Node</b>: Configure the behavior of a ggc
used in a digital twin.
</p>
<ul>
<li><b>Supplier:</b> Select a supplier to populate machine options.</li>
<li><b>SubType:</b> Select a subtype if applicable to further categorize the asset.</li>
<li><b>Model:</b> Define the specific model for more granular asset configuration.</li>
<li><b>Unit:</b> Assign a unit to standardize measurements or operations.</li>
<li><b>Speed:</b> Reaction speed of the machine in response to inputs.</li>
<li><b>Startup:</b> Define the startup time for the machine.</li>
<li><b>Warmup:</b> Define the warmup time for the machine.</li>
<li><b>Shutdown:</b> Define the shutdown time for the machine.</li>
<li><b>Cooldown:</b> Define the cooldown time for the machine.</li>
<li><b>Enable Log:</b> Enable or disable logging for the machine.</li>
<li><b>Log Level:</b> Set the log level (Info, Debug, Warn, Error).</li>
</ul>
</script>

223
ggc.js
View File

@@ -1,223 +0,0 @@
module.exports = function (RED) {
function ggc(config) {
RED.nodes.createNode(this, config);
var node = this;
try {
// Load Machine class and curve data
const Ggc = require("./dependencies/ggc/ggc");
const OutputUtils = require("../generalFunctions/helper/outputUtils");
const ggcConfig = {
general: {
name: config.name || "Unknown",
id: node.id,
logging: {
enabled: config.eneableLog,
logLevel: config.logLevel
}
},
asset: {
supplier: config.supplier || "Unknown",
type: config.machineType || "generic",
subType: config.subType || "generic",
model: config.model || "generic",
}
};
const stateConfig = {
general: {
logging: {
enabled: config.eneableLog,
logLevel: config.logLevel
}
},
movement: {
speed: Number(config.speed)
},
time: {
starting: Number(config.startup),
warmingup: Number(config.warmup),
stopping: Number(config.shutdown),
coolingdown: Number(config.cooldown)
}
};
// Create machine instance
const ggc = new Ggc(ggcConfig, stateConfig);
// put m on node memory as source
node.source = ggc;
//load output utils
const output = new OutputUtils();
function updateNodeStatus() {
try {
const mode = "auto";//ggc.currentMode;
const state = ggc.state;
const totPower = Math.round(ggc.measurements.type("power").variant("measured").position('wire').getCurrentValue()) || 0;
const SYMBOL_MAP = {
gateGroupClosed: "G1🔴 & G2🔴",
gateGroupOpened: "G1🟢 & G2🟢",
gateGroupStopped: "G1🟥 & G2🟥",
gateGroupAccelerating: "G1🟡 & G2🟡",
gateGroupDecelerating: "G1🟠 & G2🟠",
gateOneAccelerating: "G1🟡 & G2🟢",
gateTwoAccelerating: "G1🟢 & G2🟡",
gateOneDecelerating: "G1🟠 & G2🟢",
gateTwoDecelerating: "G1🟢 & G2🟠",
oneGateOpenOneGateClosed: "G1🟢 & G2🔴",
gatePushingStop: "G1⚡ & G2⚡",
unknown: "❓ & ❓",
};
symbolState = SYMBOL_MAP[state] || "Unknown";
const position = "" ; //ggc.getGatePositions();
const roundedPosition = Math.round(position * 100) / 100;
let status;
switch (state) {
// —— gateGroup states first ——
case "gateGroupClosed":
status = {
fill: "red",
shape: "dot",
text: `${mode}: ${symbolState}`
};
break;
case "gateGroupOpened":
status = {
fill: "green",
shape: "dot",
text: `${mode}: ${symbolState}`
};
break;
case "gateGroupStopped":
status = {
fill: "red",
shape: "dot",
text: `${mode}: ${symbolState}`
};
break;
case "oneGateOpenOneGateClosed":
status = {
fill: "green",
shape: "dot",
text: `${mode}: ${symbolState}`
};
break;
default:
status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
}
return status;
} catch (error) {
node.error("Error in updateNodeStatus: " + error.message);
return { fill: "red", shape: "ring", text: "Status Error" };
}
}
function tick() {
try {
const status = updateNodeStatus();
node.status(status);
//get output
const classOutput = ggc.getOutput();
const dbOutput = output.formatMsg(classOutput, ggc.config, "influxdb");
const pOutput = output.formatMsg(classOutput, ggc.config, "process");
//console.log(pOutput);
//only send output on values that changed
let msgs = [];
msgs[0] = pOutput;
msgs[1] = dbOutput;
node.send(msgs);
} catch (error) {
node.error("Error in tick function: " + error);
node.status({ fill: "red", shape: "ring", text: "Tick Error" });
}
}
// register child on first output this timeout is needed because of node - red stuff
setTimeout(
() => {
/*---execute code on first start----*/
let msgs = [];
msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upstream" };
msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downstream" };
//send msg
this.send(msgs);
},
100
);
//declare refresh interval internal node
setTimeout(
() => {
//---execute code on first start----
this.interval_id = setInterval(function(){ tick() },1000)
},
1000
);
node.on("input", function(msg, send, done) {
try {
/* Update to complete event based node by putting the tick function after an input event */
switch(msg.topic) {
case 'registerChild':
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
ggc.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
break;
case 'setMode':
ggc.setMode(msg.payload);
break;
case 'execSequence':
const { source, action, parameter } = msg.payload;
ggc.handleInput(source, action, parameter);
break;
case 'emergencystop':
const { source: esSource, action: esAction } = msg.payload;
ggc.handleInput(esSource, esAction);
break;
case 'safetySensor':
if(typeof msg.payload === "boolean") {
const safetySensor = msg.payload;
ggc.safetySensor = safetySensor;
}
break;
}
if (done) done();
} catch (error) {
node.error("Error processing input: " + error.message);
if (done) done(error);
}
});
node.on('close', function(done) {
if (node.interval_id) clearTimeout(node.interval_id);
if (node.tick_interval) clearInterval(node.tick_interval);
if (done) done();
});
} catch (error) {
node.error("Fatal error in node initialization: " + error.stack);
node.status({fill: "red", shape: "ring", text: "Fatal Error"});
}
}
RED.nodes.registerType("ggc", ggc);
};

183
mgc.html
View File

@@ -1,106 +1,72 @@
<!-- <!--
brabantse delta kleuren: | S88-niveau | Primair (blokkleur) | Tekstkleur |
#eaf4f1 | ---------------------- | ------------------- | ---------- |
#86bbdd | **Area** | `#0f52a5` | wit |
#bad33b | **Process Cell** | `#0c99d9` | wit |
#0c99d9 | **Unit** | `#50a8d9` | zwart |
#a9daee | **Equipment (Module)** | `#86bbdd` | zwart |
#0f52a5 | **Control Module** | `#a9daee` | zwart |
#50a8d9
#cade63
#4f8582
#c4cce0
--> -->
<script type="text/javascript"> <script src="/machineGroupControl/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
<script src="/machineGroupControl/configData.js"></script> <!-- Load the config script for node information -->
<script>
RED.nodes.registerType('machineGroupControl',{ RED.nodes.registerType('machineGroupControl',{
category: "EVOLV",
category: 'digital twin', color: "#50a8d9",
color: '#eaf4f1', defaults: {
defaults: { // Define default properties
name: {value:""}, name: { value: "" },
enableLog: { value: false },
logLevel: { value: "error" }, // Logger properties
}, enableLog: { value: false },
logLevel: { value: "error" },
//physicalAspect
positionVsParent: { value: "" },
positionIcon: { value: "" },
hasDistance: { value: false },
distance: { value: 0 },
distanceUnit: { value: "m" },
distanceDescription: { value: "" }
},
inputs:1, inputs:1,
outputs:4, outputs:3,
inputLabels: "Usage see manual", inputLabels: ["Input"],
outputLabels: ["process", "dbase", "upstreamParent", "downstreamParent"], outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-tachometer", icon: "font-awesome/fa-cogs",
//define label function
label: function() { label: function () {
return this.name || "MachineGroup controller"; return this.positionIcon + " " + "machineGroup";
}, },
oneditprepare: function() { oneditprepare: function() {
const node = this; // Initialize the menu data for the node
const waitForMenuData = () => {
console.log("Rotating Machine Node: Edit Prepare"); if (window.EVOLV?.nodes?.machineGroupControl?.initEditor) {
window.EVOLV.nodes.machineGroupControl.initEditor(this);
const elements = { } else {
// Basic fields setTimeout(waitForMenuData, 50);
name: document.getElementById("node-input-name"),
// Logging fields
logCheckbox: document.getElementById("node-input-enableLog"),
logLevelSelect: document.getElementById("node-input-logLevel"),
rowLogLevel: document.getElementById("row-logLevel"),
};
const projecSettingstURL = "http://localhost:1880/generalFunctions/settings/projectSettings.json";
try{
// Fetch project settings
menuUtils.fetchProjectData(projecSettingstURL)
.then((projectSettings) => {
//assign to node vars
node.configUrls = projectSettings.configUrls;
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("machineGroupControl",node.configUrls.cloud.taggcodeAPI);
node.configUrls.cloud.config = cloudConfigURL; // first call
node.configUrls.local.config = localConfigURL; // backup call
// Gets the ID of the active workspace (Flow)
const activeFlowId = RED.workspaces.active(); //fetches active flow id
node.processId = activeFlowId;
// UI elements
menuUtils.initBasicToggles(elements);
})
}catch(e){
console.log("Error fetching project settings", e);
} }
};
if(node.d){ // Wait for the menu data to be ready before initializing the editor
//this means node is disabled waitForMenuData();
console.log("Current status of node is disabled");
}
}, },
oneditsave: function(){ oneditsave: function(){
const node = this; const node = this;
//save basic properties // Validate logger properties using the logger menu
["name"].forEach( if (window.EVOLV?.nodes?.machineGroupControl?.loggerMenu?.saveEditor) {
(field) => { success = window.EVOLV.nodes.machineGroupControl.loggerMenu.saveEditor(node);
const element = document.getElementById(`node-input-${field}`);
if (element) {
node[field] = element.value || "";
}
} }
);
const logLevelElement = document.getElementById("node-input-logLevel");
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
// save position field
if (window.EVOLV?.nodes?.machineGroupControl?.positionMenu?.saveEditor) {
window.EVOLV.nodes.machineGroupControl.positionMenu.saveEditor(this);
}
} }
}); });
@@ -108,44 +74,11 @@
<script type="text/html" data-template-name="machineGroupControl"> <script type="text/html" data-template-name="machineGroupControl">
<!-------------------------------------------INPUT NAME / TYPE -----------------------------------------------> <!-- Logger fields injected here -->
<div class="form-row"> <div id="logger-fields-placeholder"></div>
<label for="node-input-name"><i class="fa fa-tag"></i>Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<hr /> <!-- Position fields injected here -->
<div id="position-fields-placeholder"></div>
<!-- loglevel checkbox -->
<div class="form-row">
<label for="node-input-enableLog"
><i class="fa fa-cog"></i> Enable Log</label
>
<input
type="checkbox"
id="node-input-enableLog"
style="width:20px; vertical-align:baseline;"
/>
<span>Enable logging</span>
</div>
<div class="form-row" id="row-logLevel">
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
<select id="node-input-logLevel" style="width:60%;">
<option value="info">Info</option>
<option value="debug">Debug</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
<!-------------------------------------------INPUT TRANSLATION TO OUTPUT ----------------------------------------------->
<hr />
<div class="form-tips"></div>
<b>Tip:</b> Ensure that the "Name" field is unique to easily identify the node.
Enable logging if you need detailed information for debugging purposes.
Choose the appropriate log level based on the verbosity required.
</div>
</script> </script>

196
mgc.js
View File

@@ -1,171 +1,39 @@
module.exports = function (RED) { const nameOfNode = 'machineGroupControl'; // this is the name of the node, it should match the file name and the node type in Node-RED
function machineGroupControl(config) { const nodeClass = require('./src/nodeClass.js'); // this is the specific node class
//create node const { MenuManager, configManager } = require('generalFunctions');
// This is the main entry point for the Node-RED node, it will register the node and setup the endpoints
module.exports = function(RED) {
// Register the node type
RED.nodes.registerType(nameOfNode, function(config) {
// Initialize the Node-RED node first
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
// Then create your custom class and attach it
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
});
//call this => node so whenver you want to call a node function type node and the function behind it // Setup admin UIs
var node = this; const menuMgr = new MenuManager(); //this will handle the menu endpoints so we can load them dynamically
const cfgMgr = new configManager(); // this will handle the config endpoints so we can load them dynamically
//fetch machine object from machine.js // Register the different menu's for the node
const MachineGroup = require('./dependencies/machineGroup/machineGroup'); RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
const OutputUtils = require("../generalFunctions/helper/outputUtils"); try {
const script = menuMgr.createEndpoint(nameOfNode, ['logger','position']);
const mgConfig = config = { res.type('application/javascript').send(script);
general: { } catch (err) {
name: config.name, res.status(500).send(`// Error generating menu: ${err.message}`);
id : config.id,
logging: {
enabled: config.loggingEnabled,
logLevel: config.logLevel,
}
},
};
//make new class on creation to work with.
const mg = new MachineGroup(mgConfig);
// put mg on node memory as source
node.source = mg;
//load output utils
const output = new OutputUtils();
//update node status
function updateNodeStatus(mg) {
const mode = mg.mode;
const scaling = mg.scaling;
const totalFlow = Math.round(mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() * 1) / 1;
const totalPower = Math.round(mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() * 1) / 1;
// Calculate total capacity based on available machines
const availableMachines = Object.values(mg.machines).filter(machine => {
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
return !(state === "off" || state === "maintenance" || mode === "maintenance");
});
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
// Determine overall status based on available machines
const status = availableMachines.length > 0
? `${availableMachines.length} machines`
: "No machines";
let scalingSymbol = '';
switch (scaling.toLowerCase()) {
case 'absolute':
scalingSymbol = 'Ⓐ'; // Clear symbol for Absolute mode
break;
case 'normalized':
scalingSymbol = 'Ⓝ'; // Clear symbol for Normalized mode
break;
default:
scalingSymbol = mode;
break;
}
// Generate status text in a single line
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
return {
fill: availableMachines.length > 0 ? "green" : "red",
shape: "dot",
text
};
} }
});
//never ending functions // Endpoint to get the configuration data for the specific node
function tick(){ RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => {
//source.tick(); try {
const status = updateNodeStatus(mg); const script = cfgMgr.createEndpoint(nameOfNode);
node.status(status); // Send the configuration data as JSON response
res.type('application/javascript').send(script);
//get output } catch (err) {
const classOutput = mg.getOutput(); res.status(500).send(`// Error generating configData: ${err.message}`);
const dbOutput = output.formatMsg(classOutput, mg.config, "influxdb");
const pOutput = output.formatMsg(classOutput, mg.config, "process");
//only send output on values that changed
let msgs = [];
msgs[0] = pOutput;
msgs[1] = dbOutput;
node.send(msgs);
} }
});
// register child on first output this timeout is needed because of node - red stuff
setTimeout(
() => {
/*---execute code on first start----*/
let msgs = [];
msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upstream" };
msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downstream" };
//send msg
this.send(msgs);
},
100
);
//declare refresh interval internal node
setTimeout(
() => {
/*---execute code on first start----*/
this.interval_id = setInterval(function(){ tick() },1000)
},
1000
);
//-------------------------------------------------------------------->>what to do on input
node.on("input", async function (msg,send,done) {
if(msg.topic == 'registerChild'){
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
mg.childRegistrationUtils.registerChild(childObj.source,msg.positionVsParent);
}
if(msg.topic == 'setMode'){
const mode = msg.payload;
const source = "parent";
mg.setMode(source,mode);
}
if(msg.topic == 'setScaling'){
const scaling = msg.payload;
mg.setScaling(scaling);
}
if(msg.topic == 'Qd'){
const Qd = parseFloat(msg.payload);
const source = "parent";
if (isNaN(Qd)) {
return mg.logger.error(`Invalid demand value: ${Qd}`);
};
try{
await mg.handleInput(source,Qd);
msg.topic = mg.config.general.name;
msg.payload = "done";
send(msg);
}catch(e){
console.log(e);
}
}
// tidy up any async code here - shutdown connections and so on.
node.on('close', function() {
clearTimeout(this.interval_id);
});
});
}
RED.nodes.registerType("machineGroupControl", machineGroupControl);
}; };

View File

@@ -17,9 +17,7 @@
"author": "Rene De Ren", "author": "Rene De Ren",
"license": "SEE LICENSE", "license": "SEE LICENSE",
"dependencies": { "dependencies": {
"generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git", "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git"
"convert": "git+https://gitea.centraal.wbd-rd.nl/RnD/convert.git",
"predict": "git+https://gitea.centraal.wbd-rd.nl/RnD/predict.git"
}, },
"node-red": { "node-red": {
"nodes": { "nodes": {

288
src/groupcontrol.test.js Normal file
View File

@@ -0,0 +1,288 @@
// ...existing code...
const MachineGroup = require('./specificClass.js');
const Machine = require('../../rotatingMachine/src/specificClass');
const Measurement = require('../../measurement/src/specificClass');
const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
const stateConfig = { time:{starting:0,warmingup:0,stopping:0,coolingdown:0}, movement:{speed:1000,mode:"staticspeed"} };
const ptConfig = {
general:{ logging:{enabled:false,logLevel:"warn"}, name:"testpt", id:"pt-1", unit:"mbar" },
functionality:{ softwareType:"measurement", role:"sensor" },
asset:{ category:"sensor", type:"pressure", model:"testmodel", supplier:"vega", unit:"mbar" },
scaling:{ absMin:0, absMax:4000 }
};
const testSuite = [];
const efficiencyComparisons = [];
function logPass(name, details="") {
const entry = { name, status:"PASS", details };
testSuite.push(entry);
console.log(`${name}${details ? `${details}` : ""}`);
}
function logFail(name, error) {
const entry = { name, status:"FAIL", details:error?.message || error };
testSuite.push(entry);
console.error(`${name}${entry.details}`);
}
function approxEqual(actual, expected, tolerancePct=1) {
const tolerance = (expected * tolerancePct) / 100;
return actual >= expected - tolerance && actual <= expected + tolerance;
}
async function sleep(ms){ return new Promise(resolve => setTimeout(resolve, ms)); }
function createMachineConfig(id,label) {
return {
general:{ logging:{enabled:false,logLevel:"warn"}, name:label, id, unit:"m3/h" },
functionality:{ softwareType:"machine", role:"rotationaldevicecontroller" },
asset:{ category:"pump", type:"centrifugal", model:"hidrostal-h05k-s03r", supplier:"hydrostal", machineCurve:specs },
mode:{
current:"auto",
allowedActions:{
auto:["execSequence","execMovement","flowMovement","statusCheck"],
virtualControl:["execMovement","statusCheck"],
fysicalControl:["statusCheck"]
},
allowedSources:{
auto:["parent","GUI"],
virtualControl:["GUI"],
fysicalControl:["fysical"]
}
},
sequences:{
startup:["starting","warmingup","operational"],
shutdown:["stopping","coolingdown","idle"],
emergencystop:["emergencystop","off"],
boot:["idle","starting","warmingup","operational"]
}
};
}
async function bootstrapGroup() {
const groupCfg = {
general:{ logging:{enabled:false,logLevel:"warn"}, name:"testmachinegroup" },
functionality:{ softwareType:"machinegroup", role:"groupcontroller" },
scaling:{ current:"normalized" },
mode:{ current:"optimalcontrol" }
};
const mg = new MachineGroup(groupCfg);
const pt = new Measurement(ptConfig);
for (let idx=1; idx<=2; idx++){
const machine = new Machine(createMachineConfig(String(idx),`machine-${idx}`), stateConfig);
mg.childRegistrationUtils.registerChild(machine,"downstream");
machine.childRegistrationUtils.registerChild(pt,"downstream");
}
pt.calculateInput(1000);
await sleep(10);
return { mg, pt };
}
function captureState(mg,label){
return {
label,
machines: Object.entries(mg.machines).map(([id,machine]) => ({
id,
state: machine.state.getCurrentState(),
position: machine.state.getCurrentPosition(),
predictedFlow: machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0,
predictedPower: machine.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() || 0
})),
totals: {
flow: mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0,
power: mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() || 0,
efficiency: mg.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue() || 0
}
};
}
async function testNormalizedScaling(mg,pt){
const label = "Normalized scaling tracks expected flow";
try{
mg.setScaling("normalized");
const dynamic = mg.calcDynamicTotals();
const checkpoints = [0,10,25,50,75,100];
for (const demand of checkpoints){
await mg.handleInput("parent", demand);
pt.calculateInput(1400);
await sleep(20);
const totals = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
const expected = dynamic.flow.min + (demand/100)*(dynamic.flow.max - dynamic.flow.min);
if(!approxEqual(totals, expected, 2)){
throw new Error(`Flow ${totals.toFixed(2)} outside expectation ${expected.toFixed(2)} @ ${demand}%`);
}
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testAbsoluteScaling(mg,pt){
const label = "Absolute scaling accepts direct flow targets";
try{
mg.setScaling("absolute");
mg.setMode("optimalcontrol");
const absMin = mg.dynamicTotals.flow.min;
const absMax = mg.dynamicTotals.flow.max;
const demandPoints = [absMin, absMin+20, (absMin+absMax)/2, absMax-20];
for(const setpoint of demandPoints){
await mg.handleInput("parent", setpoint);
pt.calculateInput(1400);
await sleep(20);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(!approxEqual(flow, setpoint, 2)){
throw new Error(`Flow ${flow.toFixed(2)} != demand ${setpoint.toFixed(2)}`);
}
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testModeTransitions(mg,pt){
const label = "Mode transitions keep machines responsive";
try{
const modes = ["optimalcontrol","prioritycontrol","prioritypercentagecontrol"];
mg.setScaling("normalized");
for(const mode of modes){
mg.setMode(mode);
await mg.handleInput("parent", 50);
pt.calculateInput(1300);
await sleep(20);
const snapshot = captureState(mg, mode);
const active = snapshot.machines.filter(m => m.state !== "idle");
if(active.length === 0){
throw new Error(`No active machines after switching to ${mode}`);
}
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testRampBehaviour(mg,pt){
const label = "Ramp up/down keeps monotonic flow";
try{
mg.setMode("optimalcontrol");
mg.setScaling("normalized");
const upDemands = [0,20,40,60,80,100];
let lastFlow = 0;
for(const demand of upDemands){
await mg.handleInput("parent", demand);
pt.calculateInput(1500);
await sleep(15);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(flow < lastFlow - 1){
throw new Error(`Flow decreased during ramp up: ${flow.toFixed(2)} < ${lastFlow.toFixed(2)}`);
}
lastFlow = flow;
}
const downDemands = [100,80,60,40,20,0];
lastFlow = Infinity;
for(const demand of downDemands){
await mg.handleInput("parent", demand);
pt.calculateInput(1200);
await sleep(15);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(flow > lastFlow + 1){
throw new Error(`Flow increased during ramp down: ${flow.toFixed(2)} > ${lastFlow.toFixed(2)}`);
}
lastFlow = flow;
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testPressureAdaptation(mg,pt){
const label = "Pressure changes update predictions";
try{
mg.setMode("optimalcontrol");
mg.setScaling("normalized");
const pressures = [800,1200,1600,2000];
let previousFlow = null;
for(const p of pressures){
pt.calculateInput(p);
await mg.handleInput("parent", 50);
await sleep(20);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(previousFlow !== null && Math.abs(flow - previousFlow) < 0.5){
throw new Error(`Flow did not react to pressure shift (${previousFlow.toFixed(2)} -> ${flow.toFixed(2)})`);
}
previousFlow = flow;
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function comparePriorityVsOptimal(mg, pt){
const label = "Priority vs Optimal efficiency comparison";
try{
mg.setScaling("normalized");
const pressures = [800, 1100, 1400, 1700];
const demands = [...Array(21)].map((_, idx) => idx * 5);
for (const pressure of pressures) {
pt.calculateInput(pressure);
await sleep(15);
for (const demand of demands) {
mg.setMode("optimalcontrol");
await mg.handleInput("parent", demand);
pt.calculateInput(pressure);
await sleep(20);
const optimalTotals = captureState(mg, `optimal-${pressure}-${demand}`).totals;
mg.setMode("prioritycontrol");
await mg.handleInput("parent", demand);
pt.calculateInput(pressure);
await sleep(20);
const priorityTotals = captureState(mg, `priority-${pressure}-${demand}`).totals;
efficiencyComparisons.push({
pressure,
demandPercent: demand,
optimalFlow: Number(optimalTotals.flow.toFixed(3)),
optimalPower: Number(optimalTotals.power.toFixed(3)),
optimalEfficiency: Number((optimalTotals.efficiency || 0).toFixed(4)),
priorityFlow: Number(priorityTotals.flow.toFixed(3)),
priorityPower: Number(priorityTotals.power.toFixed(3)),
priorityEfficiency: Number((priorityTotals.efficiency || 0).toFixed(4)),
efficiencyDelta: Number(((priorityTotals.efficiency || 0) - (optimalTotals.efficiency || 0)).toFixed(4)),
powerDelta: Number((priorityTotals.power - optimalTotals.power).toFixed(3))
});
}
}
logPass(label, "efficiencyComparisons array populated");
} catch (err) {
logFail(label, err);
}
}
async function run(){
console.log("🚀 Starting machine-group integration tests...");
const { mg, pt } = await bootstrapGroup();
await testNormalizedScaling(mg, pt);
await testAbsoluteScaling(mg, pt);
await testModeTransitions(mg, pt);
await testRampBehaviour(mg, pt);
await testPressureAdaptation(mg, pt);
await comparePriorityVsOptimal(mg, pt);
console.log("\n📋 TEST SUMMARY");
console.table(testSuite);
console.log("\n📊 efficiencyComparisons:");
console.dir(efficiencyComparisons, { depth:null });
console.log("✅ All tests completed.");
}
run().catch(err => {
console.error("💥 Test harness crashed:", err);
});
// ...existing code...
// Run all tests
run();

274
src/nodeClass.js Normal file
View File

@@ -0,0 +1,274 @@
const { outputUtils, configManager } = require("generalFunctions");
const Specific = require("./specificClass");
class nodeClass {
/**
* Create a MeasurementNode.
* @param {object} uiConfig - Node-RED node configuration.
* @param {object} RED - Node-RED runtime API.
* @param {object} nodeInstance - The Node-RED node instance.
* @param {string} nameOfNode - The name of the node, used for
*/
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
// Preserve RED reference for HTTP endpoints if needed
this.node = nodeInstance; // This is the Node-RED node instance, we can use this to send messages and update status
this.RED = RED; // This is the Node-RED runtime API, we can use this to create endpoints if needed
this.name = nameOfNode; // This is the name of the node, it should match the file name and the node type in Node-RED
this.source = null; // Will hold the specific class instance
// Load default & UI config
this._loadConfig(uiConfig, this.node);
// Instantiate core Measurement class
this._setupSpecificClass();
// Wire up event and lifecycle handlers
this._bindEvents();
this._registerChild();
this._startTickLoop();
this._attachInputHandler();
this._attachCloseHandler();
}
/**
* Load and merge default config with user-defined settings.
* @param {object} uiConfig - Raw config from Node-RED UI.
*/
_loadConfig(uiConfig, node) {
const cfgMgr = new configManager();
this.defaultConfig = cfgMgr.getConfig(this.name);
// Merge UI config over defaults
this.config = {
general: {
name: uiConfig.name,
id: node.id, // node.id is for the child registration process
unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
logging: {
enabled: uiConfig.enableLog,
logLevel: uiConfig.logLevel,
},
},
functionality: {
positionVsParent: uiConfig.positionVsParent || "atEquipment", // Default to 'atEquipment' if not set
},
};
// Utility for formatting outputs
this._output = new outputUtils();
}
_updateNodeStatus() {
//console.log('Updating node status...');
const mg = this.source;
const mode = mg.mode;
const scaling = mg.scaling;
// Add safety checks for measurements
const totalFlow = mg.measurements
?.type("flow")
?.variant("predicted")
?.position("downstream")
?.getCurrentValue() || 0;
const totalPower = mg.measurements
?.type("power")
?.variant("predicted")
?.position("atEquipment")
?.getCurrentValue() || 0;
// Calculate total capacity based on available machines with safety checks
const availableMachines = Object.values(mg.machines || {}).filter((machine) => {
// Safety check: ensure machine and machine.state exist
if (!machine || !machine.state || typeof machine.state.getCurrentState !== 'function') {
console.warn(`Machine missing or invalid:`, machine?.config?.general?.id || 'unknown');
return false;
}
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
return !(
state === "off" ||
state === "maintenance" ||
mode === "maintenance"
);
});
const totalCapacity = Math.round((mg.dynamicTotals?.flow?.max || 0) * 1) / 1;
// Determine overall status based on available machines
const status = availableMachines.length > 0
? `${availableMachines.length} machine(s) connected`
: "No machines";
let scalingSymbol = "";
switch ((scaling || "").toLowerCase()) {
case "absolute":
scalingSymbol = "Ⓐ";
break;
case "normalized":
scalingSymbol = "Ⓝ";
break;
default:
scalingSymbol = mode || "";
break;
}
const text = ` ${mode || 'Unknown'} | ${scalingSymbol}: 💨=${Math.round(totalFlow)}/${totalCapacity} | ⚡=${Math.round(totalPower)} | ${status}`;
return {
fill: availableMachines.length > 0 ? "green" : "red",
shape: "dot",
text,
};
}
/**
* Instantiate the core logic and store as source.
*/
_setupSpecificClass() {
this.source = new Specific(this.config);
this.node.source = this.source; // Store the source in the node instance for easy access
}
/**
* Bind events to Node-RED status updates. Using internal emitter. --> REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES
*/
_bindEvents() {
this.source.emitter.on("mAbs", (val) => {
this.node.status({
fill: "green",
shape: "dot",
text: `${val} ${this.config.general.unit}`,
});
});
}
/**
* Register this node as a child upstream and downstream.
* Delayed to avoid Node-RED startup race conditions.
*/
_registerChild() {
setTimeout(() => {
this.node.send([
null,
null,
{
topic: "registerChild",
payload: this.node.id,
positionVsParent:
this.config?.functionality?.positionVsParent || "atEquipment",
},
]);
}, 100);
}
/**
* Start the periodic tick loop to drive the Measurement class.
*/
_startTickLoop() {
setTimeout(() => {
this._tickInterval = setInterval(() => this._tick(), 1000);
// Update node status on nodered screen every second ( this is not the best way to do this, but it works for now)
this._statusInterval = setInterval(() => {
const status = this._updateNodeStatus();
this.node.status(status);
}, 1000);
}, 1000);
}
/**
* Execute a single tick: update measurement, format and send outputs.
*/
_tick() {
const raw = this.source.getOutput();
const processMsg = this._output.formatMsg(raw, this.config, "process");
const influxMsg = this._output.formatMsg(raw, this.config, "influxdb");
// Send only updated outputs on ports 0 & 1
this.node.send([processMsg, influxMsg]);
}
/**
* Attach the node's input handler, routing control messages to the class.
*/
_attachInputHandler() {
this.node.on(
"input",
async (msg, send, done) => {
const mg = this.source;
const RED = this.RED;
switch (msg.topic) {
case "registerChild":
console.log(`Registering child in mgc: ${msg.payload}`);
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
// Debug: Check what we're getting
console.log(`Child object:`, childObj ? 'found' : 'NOT FOUND');
console.log(`Child source:`, childObj?.source ? 'exists' : 'MISSING');
if (childObj?.source) {
console.log(`Child source type:`, childObj.source.constructor.name);
console.log(`Child has state:`, !!childObj.source.state);
}
mg.childRegistrationUtils.registerChild(
childObj.source,
msg.positionVsParent
);
// Debug: Check machines after registration
console.log(`Total machines after registration:`, Object.keys(mg.machines || {}).length);
break;
case "setMode":
const mode = msg.payload;
mg.setMode(mode);
break;
case "setScaling":
const scaling = msg.payload;
mg.setScaling(scaling);
break;
case "Qd":
const Qd = parseFloat(msg.payload);
const sourceQd = "parent";
if (isNaN(Qd)) {
return mg.logger.error(`Invalid demand value: ${Qd}`);
}
try {
await mg.handleInput(sourceQd, Qd);
msg.topic = mg.config.general.name;
msg.payload = "done";
send(msg);
} catch (e) {
console.log(e);
}
break;
default:
// Handle unknown topics if needed
mg.logger.warn(`Unknown topic: ${msg.topic}`);
break;
}
done();
}
);
}
/**
* Clean up timers and intervals when Node-RED stops the node.
*/
_attachCloseHandler() {
this.node.on("close", (done) => {
clearInterval(this._tickInterval);
clearInterval(this._statusInterval);
done();
});
}
}
module.exports = nodeClass; // Export the class for Node-RED to use

View File

@@ -1,69 +1,24 @@
/**
* @file machine.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary A class to interact and manipulate machines with a non-euclidian curve
* @description A class to interact and manipulate machines with a non-euclidian curve
* @module machineGroup
* @exports machineGroup
* @version 0.1.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - rene@thegoldenbasket.nl
*/
//load local dependencies //load local dependencies
const EventEmitter = require("events"); const EventEmitter = require("events");
const Logger = require('../../../generalFunctions/helper/logger'); const {logger,configUtils,configManager, MeasurementContainer, interpolation , childRegistrationUtils} = require('generalFunctions');
const Interpolation = require('../../../predict/dependencies/predict/interpolation');
const { MeasurementContainer } = require('../../../generalFunctions/helper/measurements/index');
//load all config modules
const defaultConfig = require('./machineGroupConfig.json');
const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
//load registration utility
const ChildRegistrationUtils = require('../../../generalFunctions/helper/childRegistrationUtils');
class MachineGroup { class MachineGroup {
constructor(machineGroupConfig = {}) { constructor(machineGroupConfig = {}) {
this.emitter = new EventEmitter(); // Own EventEmitter this.emitter = new EventEmitter(); // Own EventEmitter
this.configUtils = new ConfigUtils(defaultConfig); this.configManager = new configManager(); // Config manager to handle dynamic config loading
this.config = this.configUtils.initConfig(machineGroupConfig); this.defaultConfig = this.configManager.getConfig('machineGroupControl'); // Load default config for rotating machine ( use software type name ? )
this.configUtils = new configUtils(this.defaultConfig);// this will handle the config endpoints so we can load them dynamically
this.config = this.configUtils.initConfig(machineGroupConfig); // verify and set the config for the machine group
// Init after config is set // Init after config is set
this.logger = new Logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name); this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
// Initialize measurements // Initialize measurements
this.measurements = new MeasurementContainer(); this.measurements = new MeasurementContainer();
this.interpolation = new Interpolation(); this.interpolation = new interpolation();
// Machines and children data // Machines and child data
this.machines = {}; this.machines = {};
this.child = {}; this.child = {};
this.scaling = this.config.scaling.current; this.scaling = this.config.scaling.current;
@@ -75,18 +30,42 @@ class MachineGroup {
this.dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } , NCog : 0}; this.dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } , NCog : 0};
this.absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }}; this.absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }};
//this always last in the constructor //this always last in the constructor
this.childRegistrationUtils = new ChildRegistrationUtils(this); this.childRegistrationUtils = new childRegistrationUtils(this);
this.logger.info("MachineGroup initialized."); this.logger.info("MachineGroup initialized.");
} }
// when a child gets updated do something registerChild(child,softwareType) {
handleChildChange() { this.logger.debug('Setting up childs specific for this class');
this.absoluteTotals = this.calcAbsoluteTotals();
//for reference and not to recalc these values continiously if(softwareType == "machine"){
this.dynamicTotals = this.calcDynamicTotals(); // Check if the machine is already registered
this.machines[child.config.general.id] === undefined ? this.machines[child.config.general.id] = child : this.logger.warn(`Machine ${child.config.general.id} is already registered.`);
//listen for machine pressure changes
this.logger.debug(`Listening for pressure changes from machine ${child.config.general.id}`);
child.measurements.emitter.on("pressure.measured.differential", (eventData) => {
this.logger.debug(`Pressure update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
this.handlePressureChange();
});
child.measurements.emitter.on("pressure.measured.downstream", (eventData) => {
this.logger.debug(`Pressure update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
this.handlePressureChange();
});
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
//later change to this.handleFlowPredictionChange();
this.handlePressureChange();
});
}
} }
calcAbsoluteTotals() { calcAbsoluteTotals() {
@@ -110,6 +89,7 @@ class MachineGroup {
if( maxPower > totals.power.max ){ totals.power.max = maxPower; } if( maxPower > totals.power.max ){ totals.power.max = maxPower; }
}); });
//surplus machines for max flow and power //surplus machines for max flow and power
if( totals.flow.min < absoluteTotals.flow.min ){ absoluteTotals.flow.min = totals.flow.min; } if( totals.flow.min < absoluteTotals.flow.min ){ absoluteTotals.flow.min = totals.flow.min; }
if( totals.power.min < absoluteTotals.power.min ){ absoluteTotals.power.min = totals.power.min; } if( totals.power.min < absoluteTotals.power.min ){ absoluteTotals.power.min = totals.power.min; }
@@ -118,6 +98,29 @@ class MachineGroup {
}); });
if(absoluteTotals.flow.min === Infinity) {
this.logger.warn(`Flow min ${absoluteTotals.flow.min} is Infinity. Setting to 0.`);
absoluteTotals.flow.min = 0;
}
if(absoluteTotals.power.min === Infinity) {
this.logger.warn(`Power min ${absoluteTotals.power.min} is Infinity. Setting to 0.`);
absoluteTotals.power.min = 0;
}
if(absoluteTotals.flow.max === -Infinity) {
this.logger.warn(`Flow max ${absoluteTotals.flow.max} is -Infinity. Setting to 0.`);
absoluteTotals.flow.max = 0;
}
if(absoluteTotals.power.max === -Infinity) {
this.logger.warn(`Power max ${absoluteTotals.power.max} is -Infinity. Setting to 0.`);
absoluteTotals.power.max = 0;
}
// Place data in object for external use
this.absoluteTotals = absoluteTotals;
return absoluteTotals; return absoluteTotals;
} }
@@ -125,26 +128,39 @@ class MachineGroup {
//max and min current flow and power based on their actual pressure curve //max and min current flow and power based on their actual pressure curve
calcDynamicTotals() { calcDynamicTotals() {
const dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }, NCog : 0 }; const dynamicTotals = { flow: { min: Infinity, max: 0, act: 0 }, power: { min: Infinity, max: 0, act: 0 }, NCog : 0 };
this.logger.debug(`\n --------- Calculating dynamic totals for ${Object.keys(this.machines).length} machines. @ current pressure settings : ----------`);
Object.values(this.machines).forEach(machine => { Object.values(this.machines).forEach(machine => {
this.logger.debug(`Processing machine with id: ${machine.config.general.id}`);
this.logger.debug(`Current pressure settings: ${JSON.stringify(machine.predictFlow.currentF)}`);
//fetch min flow ever seen over all machines //fetch min flow ever seen over all machines
const minFlow = machine.predictFlow.currentFxyYMin; const minFlow = machine.predictFlow.currentFxyYMin;
const maxFlow = machine.predictFlow.currentFxyYMax; const maxFlow = machine.predictFlow.currentFxyYMax;
const minPower = machine.predictPower.currentFxyYMin; const minPower = machine.predictPower.currentFxyYMin;
const maxPower = machine.predictPower.currentFxyYMax; const maxPower = machine.predictPower.currentFxyYMax;
const actFlow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
const actPower = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue();
this.logger.debug(`Machine ${machine.config.general.id} - Min Flow: ${minFlow}, Max Flow: ${maxFlow}, Min Power: ${minPower}, Max Power: ${maxPower}, NCog: ${machine.NCog}`);
if( minFlow < dynamicTotals.flow.min ){ dynamicTotals.flow.min = minFlow; } if( minFlow < dynamicTotals.flow.min ){ dynamicTotals.flow.min = minFlow; }
if( minPower < dynamicTotals.power.min ){ dynamicTotals.power.min = minPower; } if( minPower < dynamicTotals.power.min ){ dynamicTotals.power.min = minPower; }
dynamicTotals.flow.max += maxFlow; dynamicTotals.flow.max += maxFlow;
dynamicTotals.power.max += maxPower; dynamicTotals.power.max += maxPower;
dynamicTotals.flow.act += actFlow;
dynamicTotals.power.act += actPower;
//fetch total Normalized Cog over all machines //fetch total Normalized Cog over all machines
dynamicTotals.NCog += machine.NCog; dynamicTotals.NCog += machine.NCog;
}); });
// Place data in object for external use
this.dynamicTotals = dynamicTotals;
return dynamicTotals; return dynamicTotals;
} }
@@ -174,10 +190,16 @@ class MachineGroup {
} }
handlePressureChange() { handlePressureChange() {
this.logger.info("Pressure change detected."); this.logger.info("---------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>Pressure change detected.");
this.calcDynamicTotals(); // Recalculate totals
const { flow, power } = this.calcDynamicTotals();
this.logger.debug(`Dynamic Totals after pressure change - Flow: Min ${flow.min}, Max ${flow.max}, Act ${flow.act} | Power: Min ${power.min}, Max ${power.max}, Act ${power.act}`);
this.measurements.type("flow").variant("predicted").position("downstream").value(flow.act);
this.measurements.type("power").variant("predicted").position("atEquipment").value(power.act);
const { maxEfficiency, lowestEfficiency } = this.calcGroupEfficiency(this.machines); const { maxEfficiency, lowestEfficiency } = this.calcGroupEfficiency(this.machines);
const efficiency = this.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue(); const efficiency = this.measurements.type("efficiency").variant("predicted").position("atEquipment").getCurrentValue();
this.calcDistanceBEP(efficiency,maxEfficiency,lowestEfficiency); this.calcDistanceBEP(efficiency,maxEfficiency,lowestEfficiency);
} }
@@ -206,6 +228,7 @@ class MachineGroup {
checkSpecialCases(machines, Qd) { checkSpecialCases(machines, Qd) {
Object.values(machines).forEach(machine => { Object.values(machines).forEach(machine => {
const state = machine.state.getCurrentState(); const state = machine.state.getCurrentState();
const mode = machine.currentMode; const mode = machine.currentMode;
@@ -239,16 +262,13 @@ class MachineGroup {
// Generate all possible subsets of machines (power set) // Generate all possible subsets of machines (power set)
Object.keys(machines).forEach(machineId => { Object.keys(machines).forEach(machineId => {
machineId = parseInt(machineId);
const state = machines[machineId].state.getCurrentState(); const state = machines[machineId].state.getCurrentState();
const validSourceForMode = machines[machineId].isValidSourceForMode("parent", "auto");
const validActionForMode = machines[machineId].isValidActionForMode("execSequence", "auto"); const validActionForMode = machines[machineId].isValidActionForMode("execSequence", "auto");
// Reasons why a machine is not valid for the combination // Reasons why a machine is not valid for the combination
if( state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validSourceForMode || !validActionForMode){ if( state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validActionForMode){
return; return;
} }
@@ -343,23 +363,29 @@ class MachineGroup {
} }
// -------- Mode and Input Management -------- // // -------- Mode and Input Management -------- //
isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
return allowedSourcesSet.has(source);
}
isValidActionForMode(action, mode) { isValidActionForMode(action, mode) {
const allowedActionsSet = this.config.mode.allowedActions[mode] || []; const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
return allowedActionsSet.has(action); return allowedActionsSet.has(action);
} }
setScaling(scaling) { setScaling(scaling) {
const scalingSet = new Set(defaultConfig.scaling.current.rules.values.map( (value) => value.value)); const scalingSet = new Set(this.defaultConfig.scaling.current.rules.values.map( (value) => value.value));
scalingSet.has(scaling)? this.scaling = scaling : this.logger.warn(`${scaling} is not a valid scaling option.`); scalingSet.has(scaling)? this.scaling = scaling : this.logger.warn(`${scaling} is not a valid scaling option.`);
this.logger.debug(`Scaling set to: ${scaling}`);
}
async abortActiveMovements(reason = "new demand") {
await Promise.all(Object.values(this.machines).map(async machine => {
this.logger.warn(`Aborting active movements for machine ${machine.config.general.id} due to: ${reason}`);
if (typeof machine.abortMovement === "function") {
await machine.abortMovement(reason);
}
}));
} }
//handle input from parent / user / UI //handle input from parent / user / UI
async optimalControl(Qd, powerCap = Infinity) { async optimalControl(Qd, powerCap = Infinity) {
try{ try{
//we need to force the pressures of all machines to be equal to the highest pressure measured in the group //we need to force the pressures of all machines to be equal to the highest pressure measured in the group
// this is to ensure a correct evaluation of the flow and power consumption // this is to ensure a correct evaluation of the flow and power consumption
@@ -373,20 +399,25 @@ class MachineGroup {
const maxDownstream = Math.max(...pressures.map(p => p.downstream)); const maxDownstream = Math.max(...pressures.map(p => p.downstream));
const minUpstream = Math.min(...pressures.map(p => p.upstream)); const minUpstream = Math.min(...pressures.map(p => p.upstream));
this.logger.debug(`Max downstream pressure: ${maxDownstream}, Min upstream pressure: ${minUpstream}`);
//set the pressures //set the pressures
Object.entries(this.machines).forEach(([machineId, machine]) => { Object.entries(this.machines).forEach(([machineId, machine]) => {
if(machine.state.getCurrentState() !== "operational" && machine.state.getCurrentState() !== "accelerating" && machine.state.getCurrentState() !== "decelerating"){ if(machine.state.getCurrentState() !== "operational" && machine.state.getCurrentState() !== "accelerating" && machine.state.getCurrentState() !== "decelerating"){
//Equilize pressures over all machines so we can make a proper calculation
machine.measurements.type("pressure").variant("measured").position("downstream").value(maxDownstream); machine.measurements.type("pressure").variant("measured").position("downstream").value(maxDownstream);
machine.measurements.type("pressure").variant("measured").position("upstream").value(minUpstream); machine.measurements.type("pressure").variant("measured").position("upstream").value(minUpstream);
// after updating the measurement directly we need to force the update of the value OLIFANT this is not so clear now in the code // after updating the measurement directly we need to force the update of the value OLIFANT this is not so clear now in the code
// we need to find a better way to do this but for now it works // we need to find a better way to do this but for now it works
machine.getMeasuredPressure(); machine.getMeasuredPressure();
} }
}); });
//fetch dynamic totals
const dynamicTotals = this.dynamicTotals;
//update dynamic totals
const dynamicTotals = this.calcDynamicTotals();
const machineStates = Object.entries(this.machines).reduce((acc, [machineId, machine]) => { const machineStates = Object.entries(this.machines).reduce((acc, [machineId, machine]) => {
acc[machineId] = machine.state.getCurrentState(); acc[machineId] = machine.state.getCurrentState();
return acc; return acc;
@@ -408,48 +439,48 @@ class MachineGroup {
} }
// fetch all valid combinations that meet expectations // fetch all valid combinations that meet expectations
const combinations = this.validPumpCombinations(this.machines, Qd, powerCap); const combinations = this.validPumpCombinations(this.machines, Qd, powerCap);
//
const bestResult = this.calcBestCombination(combinations, Qd); const bestResult = this.calcBestCombination(combinations, Qd);
if(bestResult.bestCombination === null){ if(bestResult.bestCombination === null){
this.logger.warn(`Demand: ${Qd.toFixed(2)} -> No valid combination found => not updating control `); this.logger.warn(`Demand: ${Qd.toFixed(2)} -> No valid combination found => not updating control `);
return; return;
} }
const debugInfo = bestResult.bestCombination.map(({ machineId, flow }) => `${machineId}: ${flow.toFixed(2)} units`).join(" | "); const debugInfo = bestResult.bestCombination.map(({ machineId, flow }) => `${machineId}: ${flow.toFixed(2)} units`).join(" | ");
this.logger.debug(`Moving to demand: ${Qd.toFixed(2)} -> Pumps: [${debugInfo}] => Total Power: ${bestResult.bestPower.toFixed(2)}`); this.logger.debug(`Moving to demand: ${Qd.toFixed(2)} -> Pumps: [${debugInfo}] => Total Power: ${bestResult.bestPower.toFixed(2)}`);
//store the total delivered power //store the total delivered power
this.measurements.type("power").variant("predicted").position("upstream").value(bestResult.bestPower); this.measurements.type("power").variant("predicted").position("atEquipment").value(bestResult.bestPower);
this.measurements.type("flow").variant("predicted").position("downstream").value(bestResult.bestFlow); this.measurements.type("flow").variant("predicted").position("downstream").value(bestResult.bestFlow);
this.measurements.type("efficiency").variant("predicted").position("downstream").value(bestResult.bestFlow / bestResult.bestPower); this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(bestResult.bestFlow / bestResult.bestPower);
this.measurements.type("Ncog").variant("predicted").position("downstream").value(bestResult.bestCog); this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(bestResult.bestCog);
await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => {
// Find the flow for this machine in the best combination
const pumpInfo = bestResult.bestCombination.find(item => item.machineId == machineId); this.logger.debug(`Searching for machine ${machineId} with state ${machineStates[machineId]} in best combination.`);
const pumpInfo = bestResult.bestCombination.find(item => item.machineId == machineId);
let flow; let flow;
if(pumpInfo !== undefined){ if(pumpInfo !== undefined){
flow = pumpInfo.flow; flow = pumpInfo.flow;
} else { } else {
this.logger.debug(`Machine ${machineId} not in best combination, setting flow to 0`); this.logger.debug(`Machine ${machineId} not in best combination, setting flow control to 0`);
flow = 0; flow = 0;
} }
if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){ if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){
await machine.handleInput("parent", "execSequence", "shutdown"); await machine.handleInput("parent", "execSequence", "shutdown");
} }
else if(machineStates[machineId] === "idle" && flow > 0){
if(machineStates[machineId] === "idle" && flow > 0){
await machine.handleInput("parent", "execSequence", "startup"); await machine.handleInput("parent", "execSequence", "startup");
}
else if(machineStates[machineId] === "operational" && flow > 0 ){
await machine.handleInput("parent", "flowMovement", flow); await machine.handleInput("parent", "flowMovement", flow);
} }
if(machineStates[machineId] === "operational" && flow > 0 ){
await machine.handleInput("parent", "flowMovement", flow);
}
})); }));
} }
catch(err){ catch(err){
this.logger.error(err); this.logger.error(err);
@@ -511,7 +542,7 @@ class MachineGroup {
.map(id => ({ id, machine: this.machines[id] })); .map(id => ({ id, machine: this.machines[id] }));
} else { } else {
machinesInPriorityOrder = Object.entries(this.machines) machinesInPriorityOrder = Object.entries(this.machines)
.map(([id, machine]) => ({ id: parseInt(id), machine })) .map(([id, machine]) => ({ id: id, machine }))
.sort((a, b) => a.id - b.id); .sort((a, b) => a.id - b.id);
} }
return machinesInPriorityOrder; return machinesInPriorityOrder;
@@ -520,10 +551,9 @@ class MachineGroup {
filterOutUnavailableMachines(list) { filterOutUnavailableMachines(list) {
const newList = list.filter(({ id, machine }) => { const newList = list.filter(({ id, machine }) => {
const state = machine.state.getCurrentState(); const state = machine.state.getCurrentState();
const validSourceForMode = machine.isValidSourceForMode("parent", "auto");
const validActionForMode = machine.isValidActionForMode("execSequence", "auto"); const validActionForMode = machine.isValidActionForMode("execSequence", "auto");
return !(state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validSourceForMode || !validActionForMode); return !(state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validActionForMode);
}); });
return newList; return newList;
} }
@@ -558,14 +588,6 @@ class MachineGroup {
// Update dynamic totals // Update dynamic totals
const dynamicTotals = this.calcDynamicTotals(); const dynamicTotals = this.calcDynamicTotals();
// Handle zero demand by shutting down all machines early exit
if (Qd <= 0) {
await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => {
if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); }
}));
return;
}
// Cap flow demand to min/max possible values // Cap flow demand to min/max possible values
Qd = this.capFlowDemand(Qd,dynamicTotals); Qd = this.capFlowDemand(Qd,dynamicTotals);
@@ -659,14 +681,16 @@ class MachineGroup {
this.logger.debug(`Priority control for demand: ${totalFlow.toFixed(2)} -> Active pumps: [${debugInfo}] => Total Power: ${totalPower.toFixed(2)}`); this.logger.debug(`Priority control for demand: ${totalFlow.toFixed(2)} -> Active pumps: [${debugInfo}] => Total Power: ${totalPower.toFixed(2)}`);
// Store measurements // Store measurements
this.measurements.type("power").variant("predicted").position("upstream").value(totalPower); this.measurements.type("power").variant("predicted").position("atEquipment").value(totalPower);
this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow); this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow);
this.measurements.type("efficiency").variant("predicted").position("downstream").value(totalFlow / totalPower); this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow / totalPower);
this.measurements.type("Ncog").variant("predicted").position("downstream").value(totalCog); this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(totalCog);
this.logger.debug(`Flow distribution: ${JSON.stringify(flowDistribution)}`);
// Apply the flow distribution to machines // Apply the flow distribution to machines
await Promise.all(flowDistribution.map(async ({ machineId, flow }) => { await Promise.all(flowDistribution.map(async ({ machineId, flow }) => {
const machine = this.machines[machineId]; const machine = this.machines[machineId];
this.logger.debug(this.machines[machineId].state);
const currentState = this.machines[machineId].state.getCurrentState(); const currentState = this.machines[machineId].state.getCurrentState();
if (flow <= 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) { if (flow <= 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) {
@@ -771,8 +795,10 @@ class MachineGroup {
// fetch and store measurements // fetch and store measurements
Object.entries(this.machines).forEach(([machineId, machine]) => { Object.entries(this.machines).forEach(([machineId, machine]) => {
const powerValue = machine.measurements.type("power").variant("predicted").position("upstream").getCurrentValue();
const powerValue = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue();
const flowValue = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); const flowValue = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
if (powerValue !== null) { if (powerValue !== null) {
totalPower.push(powerValue); totalPower.push(powerValue);
} }
@@ -781,10 +807,11 @@ class MachineGroup {
} }
}); });
this.measurements.type("power").variant("predicted").position("upstream").value(totalPower.reduce((a, b) => a + b, 0)); this.measurements.type("power").variant("predicted").position("atEquipment").value(totalPower.reduce((a, b) => a + b, 0));
this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow.reduce((a, b) => a + b, 0)); this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow.reduce((a, b) => a + b, 0));
if(totalPower.reduce((a, b) => a + b, 0) > 0){ if(totalPower.reduce((a, b) => a + b, 0) > 0){
this.measurements.type("efficiency").variant("predicted").position("downstream").value(totalFlow.reduce((a, b) => a + b, 0) / totalPower.reduce((a, b) => a + b, 0)); this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow.reduce((a, b) => a + b, 0) / totalPower.reduce((a, b) => a + b, 0));
} }
} }
@@ -793,49 +820,85 @@ class MachineGroup {
} }
} }
async handleInput(source, Qd, powerCap = Infinity, priorityList = null) { async handleInput(source, demand, powerCap = Infinity, priorityList = null) {
//abort current movements
if (!this.isValidSourceForMode(source, this.mode)) { await this.abortActiveMovements("new demand received");
this.logger.warn(`Invalid source ${source} for mode ${this.mode}`);
return;
}
const scaling = this.scaling; const scaling = this.scaling;
const mode = this.mode; const mode = this.mode;
let rawInput = Qd; const dynamicTotals = this.calcDynamicTotals();
const demandQ = parseFloat(demand);
let demandQout = 0; // keep output Q by default 0 for safety
this.logger.debug(`Handling input from ${source}: Demand = ${demand}, Power Cap = ${powerCap}, Priority List = ${priorityList}`);
switch (scaling) { switch (scaling) {
case "absolute": case "absolute":
// No scaling needed but cap range if (isNaN(demandQ)) {
if (Qd < this.absoluteTotals.flow.min) { this.logger.warn(`Invalid absolute flow demand: ${demand}. Must be a number.`);
this.logger.warn(`Flow demand ${Qd} is below minimum possible flow ${this.absoluteTotals.flow.min}. Capping to minimum flow.`); demandQout = 0;
Qd = this.absoluteTotals.flow.min; return;
} else if (Qd > this.absoluteTotals.flow.max) { }
this.logger.warn(`Flow demand ${Qd} is above maximum possible flow ${this.absoluteTotals.flow.max}. Capping to maximum flow.`);
Qd = this.absoluteTotals.flow.max; if (demandQ < absoluteTotals.flow.min) {
this.logger.warn(`Flow demand ${demandQ} is below minimum possible flow ${absoluteTotals.flow.min}. Capping to minimum flow.`);
demandQout = this.absoluteTotals.flow.min;
} else if (demandQout > absoluteTotals.flow.max) {
this.logger.warn(`Flow demand ${demandQ} is above maximum possible flow ${absoluteTotals.flow.max}. Capping to maximum flow.`);
demandQout = absoluteTotals.flow.max;
}else if(demandQout <= 0){
this.logger.debug(`Turning machines off`);
demandQout = 0;
//return early and turn all machines off
this.turnOffAllMachines();
return;
} }
break; break;
case "normalized": case "normalized":
// Scale demand to 0-100% linear between min and max flow this is auto capped
Qd = this.interpolation.interpolate_lin_single_point(Qd, 0, 100, this.dynamicTotals.flow.min, this.dynamicTotals.flow.max); this.logger.debug(`Normalizing flow demand: ${demandQ} with min: ${dynamicTotals.flow.min} and max: ${dynamicTotals.flow.max}`);
if(demand < 0){
this.logger.debug(`Turning machines off`);
demandQout = 0;
//return early and turn all machines off
this.turnOffAllMachines();
return;
}
else{
// Scale demand to 0-100% linear between min and max flow this is auto capped
demandQout = this.interpolation.interpolate_lin_single_point(demandQ, 0, 100, dynamicTotals.flow.min, dynamicTotals.flow.max );
this.logger.debug(`Normalized flow demand ${demandQ}% to: ${demandQout} Q units`);
}
break; break;
} }
// Execute control based on mode
switch(mode) { switch(mode) {
case "prioritycontrol": case "prioritycontrol":
await this.equalFlowControl(Qd,powerCap,priorityList); this.logger.debug(`Calculating prio control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`);
await this.equalFlowControl(demandQout,powerCap,priorityList);
break; break;
case "prioritypercentagecontrol": case "prioritypercentagecontrol":
this.logger.debug(`Calculating prio percentage control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`);
if(scaling !== "normalized"){ if(scaling !== "normalized"){
this.logger.warn("Priority percentage control is only valid with normalized scaling."); this.logger.warn("Priority percentage control is only valid with normalized scaling.");
return; return;
} }
await this.prioPercentageControl(rawInput,priorityList); await this.prioPercentageControl(demandQout,priorityList);
break; break;
case "optimalcontrol": case "optimalcontrol":
await this.optimalControl(Qd,powerCap); this.logger.debug(`Calculating optimal control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`);
await this.optimalControl(demandQout,powerCap);
break;
default:
this.logger.warn(`${mode} is not a valid mode.`);
break; break;
} }
@@ -846,8 +909,14 @@ class MachineGroup {
} }
setMode(source,mode) { async turnOffAllMachines(){
this.isValidSourceForMode(source, mode) ? this.mode = mode : this.logger.warn(`Invalid source ${source} for mode ${mode}`); await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => {
if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); }
}));
}
setMode(mode) {
this.mode = mode;
} }
getOutput() { getOutput() {
@@ -860,6 +929,7 @@ class MachineGroup {
this.measurements.getVariants(type).forEach(variant => { this.measurements.getVariants(type).forEach(variant => {
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue(); const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
const atEquipmentVal = this.measurements.type(type).variant(variant).position("atEquipment").getCurrentValue();
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue(); const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
if (downstreamVal != null) { if (downstreamVal != null) {
@@ -868,6 +938,9 @@ class MachineGroup {
if (upstreamVal != null) { if (upstreamVal != null) {
output[`upstream_${variant}_${type}`] = upstreamVal; output[`upstream_${variant}_${type}`] = upstreamVal;
} }
if (atEquipmentVal != null) {
output[`atEquipment_${variant}_${type}`] = atEquipmentVal;
}
if (downstreamVal != null && upstreamVal != null) { if (downstreamVal != null && upstreamVal != null) {
const diffVal = this.measurements.type(type).variant(variant).difference().value; const diffVal = this.measurements.type(type).variant(variant).difference().value;
output[`differential_${variant}_${type}`] = diffVal; output[`differential_${variant}_${type}`] = diffVal;
@@ -891,31 +964,31 @@ class MachineGroup {
} }
module.exports = MachineGroup; module.exports = MachineGroup;
/* /*
const Machine = require('../../../rotatingMachine/dependencies/machine/machine');
const Measurement = require('../../../measurement/dependencies/measurement/measurement');
const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json');
const power = require("../../../convert/dependencies/definitions/power");
const { machine } = require("os");
function createBaseMachineConfig(name,specs) { const Machine = require('../../rotatingMachine/src/specificClass');
const Measurement = require('../../measurement/src/specificClass');
const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
const { max } = require("mathjs");
function createBaseMachineConfig(machineNum, name,specs) {
return { return {
general: { general: {
logging: { enabled: true, logLevel: "warn" }, logging: { enabled: true, logLevel: "debug" },
name: name, name: name,
id: machineNum,
unit: "m3/h" unit: "m3/h"
}, },
functionality: { functionality: {
softwareType: "machine", softwareType: "machine",
role: "RotationalDeviceController" role: "rotationaldevicecontroller"
}, },
asset: { asset: {
type: "pump", category: "pump",
subType: "Centrifugal", type: "centrifugal",
model: "TestModel", model: "hidrostal-h05k-s03r",
supplier: "Hydrostal", supplier: "hydrostal",
machineCurve: specs[0].machineCurve machineCurve: specs
}, },
mode: { mode: {
current: "auto", current: "auto",
@@ -939,6 +1012,23 @@ function createBaseMachineConfig(name,specs) {
}; };
} }
function createStateConfig(){
return {
time:{
starting: 1,
stopping: 1,
warmingup: 1,
coolingdown: 1,
emergencystop: 1
},
movement:{
mode:"dynspeed",
speed:100,
maxSpeed: 1000
}
}
};
function createBaseMachineGroupConfig(name) { function createBaseMachineGroupConfig(name) {
return { return {
general: { general: {
@@ -946,8 +1036,8 @@ function createBaseMachineGroupConfig(name) {
name: name name: name
}, },
functionality: { functionality: {
softwareType: "machineGroup", softwareType: "machinegroup",
role: "GroupController" role: "groupcontroller"
}, },
scaling: { scaling: {
current: "normalized" current: "normalized"
@@ -958,22 +1048,30 @@ function createBaseMachineGroupConfig(name) {
}; };
} }
const machineGroupConfig = createBaseMachineGroupConfig("TestMachineGroup"); const machineGroupConfig = createBaseMachineGroupConfig("testmachinegroup");
const machineConfig = createBaseMachineConfig("TestMachine",specs); const stateConfigs = {};
const machineConfigs = {};
stateConfigs[1] = createStateConfig();
stateConfigs[2] = createStateConfig();
machineConfigs[1]= createBaseMachineConfig("asdfkj;asdf","testmachine",specs);
machineConfigs[2] = createBaseMachineConfig("asdfkj;asdf2","testmachine2",specs);
const ptConfig = { const ptConfig = {
general: { general: {
logging: { enabled: true, logLevel: "debug" }, logging: { enabled: true, logLevel: "debug" },
name: "TestPT", name: "testpt",
id: "0",
unit: "mbar", unit: "mbar",
}, },
functionality: { functionality: {
softwareType: "measurement", softwareType: "measurement",
role: "Sensor" role: "sensor"
}, },
asset: { asset: {
type: "sensor", category: "sensor",
subType: "pressure", type: "pressure",
model: "TestModel", model: "testmodel",
supplier: "vega" supplier: "vega"
}, },
scaling:{ scaling:{
@@ -986,15 +1084,17 @@ async function makeMachines(){
const mg = new MachineGroup(machineGroupConfig); const mg = new MachineGroup(machineGroupConfig);
const pt1 = new Measurement(ptConfig); const pt1 = new Measurement(ptConfig);
const numofMachines = 2; const numofMachines = 2;
for(let i = 0; i < numofMachines; i++){ for(let i = 1; i <= numofMachines; i++){
const machine = new Machine(machineConfig); const machine = new Machine(machineConfigs[i],stateConfigs[i]);
//mg.machines[i] = machine; //mg.machines[i] = machine;
mg.childRegistrationUtils.registerChild(machine, "downstream"); mg.childRegistrationUtils.registerChild(machine, "downstream");
} }
mg.machines[1].childRegistrationUtils.registerChild(pt1, "downstream");
mg.machines[2].childRegistrationUtils.registerChild(pt1, "downstream");
mg.setMode("parent","prioritycontrol"); Object.keys(mg.machines).forEach(machineId => {
mg.machines[machineId].childRegistrationUtils.registerChild(pt1, "downstream");
});
mg.setMode("prioritycontrol");
mg.setScaling("normalized"); mg.setScaling("normalized");
const absMax = mg.dynamicTotals.flow.max; const absMax = mg.dynamicTotals.flow.max;
@@ -1003,14 +1103,13 @@ async function makeMachines(){
const percMax = 100; const percMax = 100;
try{ try{
/* /*
for(let demand = mg.dynamicTotals.flow.min ; demand <= mg.dynamicTotals.flow.max ; demand += 2){ for(let demand = mg.dynamicTotals.flow.min ; demand <= mg.dynamicTotals.flow.max ; demand += 2){
//set pressure //set pressure
console.log("------------------------------------"); console.log("------------------------------------");
await mg.handleInput("parent",demand); await mg.handleInput("parent",demand);
pt1.calculateInput(1400); pt1.calculateInput(1400);
console.log("Waiting for 0.2 sec ");
//await new Promise(resolve => setTimeout(resolve, 200)); //await new Promise(resolve => setTimeout(resolve, 200));
console.log("------------------------------------"); console.log("------------------------------------");
@@ -1023,22 +1122,24 @@ async function makeMachines(){
await mg.handleInput("parent",demand); await mg.handleInput("parent",demand);
pt1.calculateInput(1400); pt1.calculateInput(1400);
console.log("Waiting for 0.2 sec ");
//await new Promise(resolve => setTimeout(resolve, 200)); //await new Promise(resolve => setTimeout(resolve, 200));
console.log("------------------------------------"); console.log("------------------------------------");
} }
//*//*
*//*
for(let demand = 0 ; demand <= 100 ; demand += 1){
//set pressure
console.log("------------------------------------");
for(let demand = 0 ; demand <= 50 ; demand += 1){
//set pressure
console.log(`TESTING: processing demand of ${demand}`);
await mg.handleInput("parent",demand); await mg.handleInput("parent",demand);
Object.keys(mg.machines).forEach(machineId => {
console.log(mg.machines[machineId].state.getCurrentState());
});
console.log(`updating pressure to 1400 mbar`);
pt1.calculateInput(1400); pt1.calculateInput(1400);
console.log("Waiting for 0.2 sec ");
//await new Promise(resolve => setTimeout(resolve, 200));
console.log("------------------------------------"); console.log("------------------------------------");
} }
@@ -1053,4 +1154,5 @@ async function makeMachines(){
makeMachines(); makeMachines();
//*/
//*/