Compare commits
60 Commits
dbc36c2f57
...
dev-Pieter
| Author | SHA1 | Date | |
|---|---|---|---|
| d5d078413c | |||
| 17662ef7cb | |||
| f653a1e98c | |||
| 3886277616 | |||
| 83018fabe0 | |||
| e72579e5d0 | |||
| 0fb42865ff | |||
| b2b811e802 | |||
| bde2dcf7d8 | |||
| 76570280bc | |||
| d7017b5d33 | |||
| f93603c182 | |||
| c261335df5 | |||
| a41f053d5d | |||
| 8d7d98f126 | |||
| 3f90685834 | |||
| efc97d6cd1 | |||
|
|
d72bfd5560 | ||
| 6d30e25daa | |||
| 16e202e841 | |||
|
|
241ed1d3cb | ||
| 3876f86530 | |||
| 56be0f1840 | |||
|
|
a30f2c90f4 | ||
| 302e122387 | |||
|
|
50f99fa642 | ||
| 494a688583 | |||
| c512c96636 | |||
|
|
eb15da2a63 | ||
| 6dcd3c3d26 | |||
| 958ec2269c | |||
|
|
83ca429bf5 | ||
|
|
222d0f56fc | ||
|
|
e1c6124cf0 | ||
|
|
e87f9da4bf | ||
| 0bccad05f8 | |||
| 7191e57aea | |||
| aec2d3692d | |||
|
|
7061d6a539 | ||
| 71643375fc | |||
|
|
2540d19b76 | ||
| f13ee68938 | |||
| 475caa90db | |||
| 9aa38f9000 | |||
| 4a6273b037 | |||
| 8c9301b128 | |||
| 30908365ba | |||
| 7cdfc87c83 | |||
| 839ae2f3da | |||
| 950ca2b6b4 | |||
| 0a9d4b1dda | |||
|
|
4665949c88 | ||
|
|
a2018509ef | ||
|
|
5ca7889af1 | ||
|
|
73f518ecc7 | ||
|
|
63c5463160 | ||
|
|
fe9a2c0e7c | ||
|
|
3198690a81 | ||
|
|
9e74f850c5 | ||
|
|
d409e16cc4 |
98
LICENSE
98
LICENSE
@@ -1,9 +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 RnD
|
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 of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
2.Draagwijdte van de uit hoofde van de licentie verleende rechten
|
||||||
|
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:
|
||||||
|
— 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.
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
3.Mededeling van de broncode
|
||||||
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
134
datasets/assetData/assetData.json
Normal file
134
datasets/assetData/assetData.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"suppliers": [
|
||||||
|
{
|
||||||
|
"name": "Vega",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Sensor",
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "Temperature",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 10",
|
||||||
|
"units": ["°C", "°F"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaTemp 20",
|
||||||
|
"units": ["°C", "°F"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pressure",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaPressure 10",
|
||||||
|
"units": ["bar", "mbar", "psi"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaPressure 20",
|
||||||
|
"units": ["bar", "mbar", "psi"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flow",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaFlow 10",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaFlow 20",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Level",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaLevel 10",
|
||||||
|
"units": ["m", "ft", "mm"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaLevel 20",
|
||||||
|
"units": ["m", "ft", "mm"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Quantity (oxygen)",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaOxySense 10",
|
||||||
|
"units": ["g/m³", "mol/m³"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Quantity (TSS)",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaSolidsProbe",
|
||||||
|
"units": ["g/m³"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hidrostal",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Pumps",
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "Centrifugal",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "hidrostal-pump-001",
|
||||||
|
"name": "hidrostal-H05K-S03R",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Binder Engineering",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Valves",
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "Gate",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "binder-valve-001",
|
||||||
|
"name": "ECDV",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jet",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "binder-valve-002",
|
||||||
|
"name": "JCV",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
datasets/assetData/curves/ECDV.json
Normal file
16
datasets/assetData/curves/ECDV.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"1.204": {
|
||||||
|
"125": {
|
||||||
|
"x": [0,10,20,30,40,50,60,70,80,90,100],
|
||||||
|
"y": [0,18,50,95,150,216,337,564,882,1398,1870]
|
||||||
|
},
|
||||||
|
"150": {
|
||||||
|
"x": [0,10,20,30,40,50,60,70,80,90,100],
|
||||||
|
"y": [0,25,73,138,217,314,490,818,1281,2029,2715]
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"x": [0,10,20,30,40,50,60,70,80,90,100],
|
||||||
|
"y": [0,155,443,839,1322,1911,2982,4980,7795,12349,16524]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1062
datasets/assetData/curves/hidrostal-H05K-S03R.json
Normal file
1062
datasets/assetData/curves/hidrostal-H05K-S03R.json
Normal file
File diff suppressed because it is too large
Load Diff
124
datasets/assetData/curves/index.js
Normal file
124
datasets/assetData/curves/index.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class AssetLoader {
|
||||||
|
constructor() {
|
||||||
|
this.relPath = './'
|
||||||
|
this.baseDir = path.resolve(__dirname, this.relPath);
|
||||||
|
this.cache = new Map(); // Cache loaded JSON files for better performance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a specific curve by type
|
||||||
|
* @param {string} curveType - The curve identifier (e.g., 'hidrostal-H05K-S03R')
|
||||||
|
* @returns {Object|null} The curve data object or null if not found
|
||||||
|
*/
|
||||||
|
loadCurve(curveType) {
|
||||||
|
return this.loadAsset('curves', curveType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load any asset from a specific dataset folder
|
||||||
|
* @param {string} datasetType - The dataset folder name (e.g., 'curves', 'assetData')
|
||||||
|
* @param {string} assetId - The specific asset identifier
|
||||||
|
* @returns {Object|null} The asset data object or null if not found
|
||||||
|
*/
|
||||||
|
loadAsset(datasetType, assetId) {
|
||||||
|
//const cacheKey = `${datasetType}/${assetId}`;
|
||||||
|
const cacheKey = `${assetId}`;
|
||||||
|
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (this.cache.has(cacheKey)) {
|
||||||
|
return this.cache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = path.join(this.baseDir, `${assetId}.json`);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.warn(`Asset not found: ${filePath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and parse JSON
|
||||||
|
const rawData = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const assetData = JSON.parse(rawData);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this.cache.set(cacheKey, assetData);
|
||||||
|
|
||||||
|
return assetData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading asset ${cacheKey}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available assets in a dataset
|
||||||
|
* @param {string} datasetType - The dataset folder name
|
||||||
|
* @returns {string[]} Array of available asset IDs
|
||||||
|
*/
|
||||||
|
getAvailableAssets(datasetType) {
|
||||||
|
try {
|
||||||
|
const datasetPath = path.join(this.baseDir, datasetType);
|
||||||
|
|
||||||
|
if (!fs.existsSync(datasetPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.readdirSync(datasetPath)
|
||||||
|
.filter(file => file.endsWith('.json'))
|
||||||
|
.map(file => file.replace('.json', ''));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading dataset ${datasetType}:`, error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cache (useful for development/testing)
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export a singleton instance
|
||||||
|
const assetLoader = new AssetLoader();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
AssetLoader,
|
||||||
|
assetLoader,
|
||||||
|
// Convenience methods for backward compatibility
|
||||||
|
loadCurve: (curveType) => assetLoader.loadCurve(curveType),
|
||||||
|
loadAsset: (datasetType, assetId) => assetLoader.loadAsset(datasetType, assetId),
|
||||||
|
getAvailableAssets: (datasetType) => assetLoader.getAvailableAssets(datasetType)
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Example usage in your scripts
|
||||||
|
const loader = new AssetLoader();
|
||||||
|
|
||||||
|
// Load a specific curve
|
||||||
|
const curve = loader.loadCurve('hidrostal-H05K-S03R');
|
||||||
|
if (curve) {
|
||||||
|
console.log('Curve loaded:', curve);
|
||||||
|
} else {
|
||||||
|
console.log('Curve not found');
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
// Load any asset from any dataset
|
||||||
|
const someAsset = loadAsset('assetData', 'some-asset-id');
|
||||||
|
|
||||||
|
// Get list of available curves
|
||||||
|
const availableCurves = getAvailableAssets('curves');
|
||||||
|
console.log('Available curves:', availableCurves);
|
||||||
|
|
||||||
|
// Using the class directly for more control
|
||||||
|
const { AssetLoader } = require('./index.js');
|
||||||
|
const customLoader = new AssetLoader();
|
||||||
|
const data = customLoader.loadCurve('hidrostal-H05K-S03R');
|
||||||
|
*/
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "DAB Evosta 2 20-75",
|
|
||||||
"description": "N/A",
|
|
||||||
"machineCurve": {
|
|
||||||
"np": {
|
|
||||||
"200": {
|
|
||||||
"x": [0, 20, 40, 60, 80, 100],
|
|
||||||
"y": [5, 8, 12, 15, 17, 18]
|
|
||||||
},
|
|
||||||
"300": {
|
|
||||||
"x": [0, 20, 40, 60, 80, 100],
|
|
||||||
"y": [20, 28, 32, 34, 35, 35]
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"x": [0, 20, 40, 60, 80, 100],
|
|
||||||
"y": [35, 38, 42, 45, 47, 48]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nq": {
|
|
||||||
"200": {
|
|
||||||
"x": [0, 20, 40, 60, 80, 100],
|
|
||||||
"y": [0, 0.4, 0.8, 1.2, 1.6, 2.0]
|
|
||||||
},
|
|
||||||
"300": {
|
|
||||||
"x": [0, 20, 40, 60, 80, 100],
|
|
||||||
"y": [0, 0.72, 1.44, 2.16, 2.88, 3.6]
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"x": [0, 20, 40, 60, 80, 100],
|
|
||||||
"y": [0, 0.8, 1.6, 2.4, 3.2, 4.0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "centrifugal pumps"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "centrifugal pumps"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "hydrostal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "DAB"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "SDM120MODBUS",
|
|
||||||
"description": "De SDM120MODBUS kWh meter is een kWh meter die geschikt is voor het meten van zowel verbruik als teruglevering van stroom. Dat maakt deze meter ook zeer geschikt in combinatie met zonnepanelen."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "power"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "Proline Promag W 400",
|
|
||||||
"description": "A flow meter used for measuring the flow of liquids in various industrial applications."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "flow"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "vega"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "eh"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "eastron"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "Vegabar 14",
|
|
||||||
"description": "N/A"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "pressure"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "flow"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "temperature"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "VegaTemp 10",
|
|
||||||
"description": "Low cost sensor for general purpose applications."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VegaTemp 20",
|
|
||||||
"description": "High accuracy sensor for laboratory applications."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VegaTemp 30",
|
|
||||||
"description": "High accuracy sensor for industrial applications."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VegaTemp 40",
|
|
||||||
"description": "High accuracy sensor for environmental monitoring."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VegaTemp 50",
|
|
||||||
"description": "High accuracy temperature sensor for industrial applications."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VegaTherm 22",
|
|
||||||
"description": "Compact sensor ideal for environmental monitoring."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "VegaTemp 100",
|
|
||||||
"description": "Robust sensor designed for high temperature ranges."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
1
datasets/tagcodeapp_assets.json
Normal file
1
datasets/tagcodeapp_assets.json
Normal file
File diff suppressed because one or more lines are too long
229
datasets/tagcodeapp_product_models.json
Normal file
229
datasets/tagcodeapp_product_models.json
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Product modellen succesvol opgehaald.",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Macbook Air 12",
|
||||||
|
"product_model_subtype_id": "1",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "1",
|
||||||
|
"product_model_status": null,
|
||||||
|
"vendor_name": "Apple",
|
||||||
|
"product_subtype_name": "Laptop",
|
||||||
|
"product_model_meta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "Macbook Air 13",
|
||||||
|
"product_model_subtype_id": "1",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "1",
|
||||||
|
"product_model_status": null,
|
||||||
|
"vendor_name": "Apple",
|
||||||
|
"product_subtype_name": "Laptop",
|
||||||
|
"product_model_meta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "AirMac 1 128 GB White",
|
||||||
|
"product_model_subtype_id": "2",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "1",
|
||||||
|
"product_model_status": null,
|
||||||
|
"vendor_name": "Apple",
|
||||||
|
"product_subtype_name": "Desktop",
|
||||||
|
"product_model_meta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"name": "AirMac 2 256 GB Black",
|
||||||
|
"product_model_subtype_id": "2",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "1",
|
||||||
|
"product_model_status": null,
|
||||||
|
"vendor_name": "Apple",
|
||||||
|
"product_subtype_name": "Desktop",
|
||||||
|
"product_model_meta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"name": "AirMac 2 256 GB White",
|
||||||
|
"product_model_subtype_id": "2",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "1",
|
||||||
|
"product_model_status": null,
|
||||||
|
"vendor_name": "Apple",
|
||||||
|
"product_subtype_name": "Desktop",
|
||||||
|
"product_model_meta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6",
|
||||||
|
"name": "Vegabar 14",
|
||||||
|
"product_model_subtype_id": "3",
|
||||||
|
"product_model_description": "vegabar 14",
|
||||||
|
"vendor_id": "4",
|
||||||
|
"product_model_status": "Actief",
|
||||||
|
"vendor_name": "vega",
|
||||||
|
"product_subtype_name": "pressure",
|
||||||
|
"product_model_meta": {
|
||||||
|
"machineCurve": {
|
||||||
|
"np": {
|
||||||
|
"700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
12.962460720759278,
|
||||||
|
20.65443723573673,
|
||||||
|
31.029351002816465,
|
||||||
|
44.58926412111886,
|
||||||
|
62.87460150792057
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
13.035157335397209,
|
||||||
|
20.74906989186132,
|
||||||
|
31.029351002816465,
|
||||||
|
44.58926412111886,
|
||||||
|
62.87460150792057
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
13.064663380158798,
|
||||||
|
20.927197054134297,
|
||||||
|
31.107126521989933,
|
||||||
|
44.58926412111886,
|
||||||
|
62.87460150792057
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1000": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
13.039271391128953,
|
||||||
|
21.08680188366637,
|
||||||
|
31.30899920405947,
|
||||||
|
44.58926412111886,
|
||||||
|
62.87460150792057
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1100": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
12.940075520572446,
|
||||||
|
21.220547481589954,
|
||||||
|
31.51468295656385,
|
||||||
|
44.621326083982,
|
||||||
|
62.87460150792057
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nq": {
|
||||||
|
"700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
119.13938764447377,
|
||||||
|
150.12178608265387,
|
||||||
|
178.82698019104356,
|
||||||
|
202.3699313222398,
|
||||||
|
227.06382297856618
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
112.59072109293984,
|
||||||
|
148.15847460389205,
|
||||||
|
178.82698019104356,
|
||||||
|
202.3699313222398,
|
||||||
|
227.06382297856618
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
24.59,
|
||||||
|
49.18,
|
||||||
|
73.77,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
105.6217241180404,
|
||||||
|
144.00502117747064,
|
||||||
|
177.15212647335034,
|
||||||
|
202.3699313222398,
|
||||||
|
227.06382297856618
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7",
|
||||||
|
"name": "Vegabar 10",
|
||||||
|
"product_model_subtype_id": "3",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "4",
|
||||||
|
"product_model_status": "Actief",
|
||||||
|
"vendor_name": "vega",
|
||||||
|
"product_subtype_name": "pressure",
|
||||||
|
"product_model_meta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8",
|
||||||
|
"name": "VegaFlow 10",
|
||||||
|
"product_model_subtype_id": "4",
|
||||||
|
"product_model_description": null,
|
||||||
|
"vendor_id": "4",
|
||||||
|
"product_model_status": "Actief",
|
||||||
|
"vendor_name": "vega",
|
||||||
|
"product_subtype_name": "flow",
|
||||||
|
"product_model_meta": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
72
index.js
72
index.js
@@ -3,72 +3,44 @@
|
|||||||
* -----------------------------------------------------------
|
* -----------------------------------------------------------
|
||||||
* Central barrel file for re-exporting helpers and configurations.
|
* Central barrel file for re-exporting helpers and configurations.
|
||||||
* Provides both namespace exports and dynamic loading capabilities.
|
* Provides both namespace exports and dynamic loading capabilities.
|
||||||
|
* now we can load modules like this:
|
||||||
|
* const { menuUtils, outputUtils } = require('generalFunctions');
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Core helper modules
|
// Core helper modules
|
||||||
const menuUtils = require('./src/helper/menuUtils.js');
|
|
||||||
const outputUtils = require('./src/helper/outputUtils.js');
|
const outputUtils = require('./src/helper/outputUtils.js');
|
||||||
const logger = require('./src/helper/logger.js');
|
const logger = require('./src/helper/logger.js');
|
||||||
const validation = require('./src/helper/validationUtils.js');
|
const validation = require('./src/helper/validationUtils.js');
|
||||||
const configUtils = require('./src/helper/configUtils.js');
|
const configUtils = require('./src/helper/configUtils.js');
|
||||||
|
const assertions = require('./src/helper/assertionUtils.js')
|
||||||
|
|
||||||
// Domain-specific modules
|
// Domain-specific modules
|
||||||
const measurements = require('./src/measurements/index.js');
|
const { MeasurementContainer } = require('./src/measurements/index.js');
|
||||||
const nrmse = require('./src/nrmse/ErrorMetrics.js');
|
const configManager = require('./src/configs/index.js');
|
||||||
|
const nrmse = require('./src/nrmse/errorMetrics.js');
|
||||||
const state = require('./src/state/state.js');
|
const state = require('./src/state/state.js');
|
||||||
|
const convert = require('./src/convert/index.js');
|
||||||
// Configuration loader with error handling
|
const MenuManager = require('./src/menu/index.js');
|
||||||
function loadConfig(path) {
|
const predict = require('./src/predict/predict_class.js');
|
||||||
try {
|
const interpolation = require('./src/predict/interpolation.js');
|
||||||
return require(path);
|
const childRegistrationUtils = require('./src/helper/childRegistrationUtils.js');
|
||||||
} catch (error) {
|
const { loadCurve } = require('./datasets/assetData/curves/index.js');
|
||||||
console.warn(`Failed to load config: ${path}`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configurations
|
|
||||||
const configs = {
|
|
||||||
get projectSettings() {
|
|
||||||
return loadConfig('./configs/projectSettings.json');
|
|
||||||
},
|
|
||||||
get measurementConfig() {
|
|
||||||
return loadConfig('./configs/measurementConfig.json');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dynamic loaders with validation
|
|
||||||
function loadHelper(name) {
|
|
||||||
if (!name || typeof name !== 'string') {
|
|
||||||
throw new Error('Helper name must be a non-empty string');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return require(`./src/helper/${name}.js`);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to load helper "${name}": ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadAssetDatasets() {
|
|
||||||
try {
|
|
||||||
return require('./datasets/assetData/suppliers.json');
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to load asset datasets: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export everything
|
// Export everything
|
||||||
module.exports = {
|
module.exports = {
|
||||||
menuUtils,
|
predict,
|
||||||
|
interpolation,
|
||||||
|
configManager,
|
||||||
outputUtils,
|
outputUtils,
|
||||||
configUtils,
|
configUtils,
|
||||||
logger,
|
logger,
|
||||||
validation,
|
validation,
|
||||||
measurements,
|
assertions,
|
||||||
|
MeasurementContainer,
|
||||||
nrmse,
|
nrmse,
|
||||||
state,
|
state,
|
||||||
configs,
|
convert,
|
||||||
loadHelper,
|
MenuManager,
|
||||||
loadAssetDatasets
|
childRegistrationUtils,
|
||||||
};
|
loadCurve
|
||||||
|
};
|
||||||
|
|||||||
76
src/configs/index.js
Normal file
76
src/configs/index.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class ConfigManager {
|
||||||
|
constructor(relPath = '.') {
|
||||||
|
this.configDir = path.resolve(__dirname, relPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a configuration file by name
|
||||||
|
* @param {string} configName - Name of the config file (without .json extension)
|
||||||
|
* @returns {Object} Parsed configuration object
|
||||||
|
*/
|
||||||
|
getConfig(configName) {
|
||||||
|
try {
|
||||||
|
const configPath = path.resolve(this.configDir, `${configName}.json`);
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf8');
|
||||||
|
return JSON.parse(configData);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to load config '${configName}': ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available configuration files
|
||||||
|
* @returns {Array<string>} Array of config names (without .json extension)
|
||||||
|
*/
|
||||||
|
getAvailableConfigs() {
|
||||||
|
try {
|
||||||
|
const resolvedDir = path.resolve(this.configDir);
|
||||||
|
const files = fs.readdirSync(resolvedDir);
|
||||||
|
return files
|
||||||
|
.filter(file => file.endsWith('.json'))
|
||||||
|
.map(file => path.basename(file, '.json'));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read config directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific config exists
|
||||||
|
* @param {string} configName - Name of the config file
|
||||||
|
* @returns {boolean} True if config exists
|
||||||
|
*/
|
||||||
|
hasConfig(configName) {
|
||||||
|
const configPath = path.resolve(this.configDir, `${configName}.json`);
|
||||||
|
return fs.existsSync(configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
createEndpoint(nodeName) {
|
||||||
|
try {
|
||||||
|
// Load the config for this node
|
||||||
|
const config = this.getConfig(nodeName);
|
||||||
|
|
||||||
|
// Convert config to JSON
|
||||||
|
const configJSON = JSON.stringify(config, null, 2);
|
||||||
|
|
||||||
|
// Assemble the complete script
|
||||||
|
return `
|
||||||
|
// Create the namespace structure
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
|
||||||
|
// Inject the pre-loaded config data directly into the namespace
|
||||||
|
window.EVOLV.nodes.${nodeName}.config = ${configJSON};
|
||||||
|
|
||||||
|
console.log('${nodeName} config loaded and endpoint created');
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to create endpoint for '${nodeName}': ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConfigManager;
|
||||||
216
src/configs/machineGroupControl.json
Normal file
216
src/configs/machineGroupControl.json
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": "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."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"positionVsParent":{
|
||||||
|
"default":"atEquipment",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "atEquipment",
|
||||||
|
"description": "The node is connected at the equipment level and is responsible for controlling or monitoring the equipment as a whole."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "upstream",
|
||||||
|
"description": "The node is connected in a downstream position, indicating it is responsible for monitoring or controlling processes that occur after the equipment's operation, such as product flow or output."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "downstream",
|
||||||
|
"description": "The node is connected in an upstream position, indicating it is responsible for monitoring or controlling processes that occur before the equipment's operation, such as input flow or supply."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or 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 0–100% 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"name": {
|
"name": {
|
||||||
"default": "Measurement Configuration",
|
"default": "Sensor",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A human-readable name or label for this measurement configuration."
|
"description": "A human-readable name or label for this measurement configuration."
|
||||||
@@ -70,6 +70,34 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Indicates the role this configuration plays (e.g., sensor, controller, etc.)."
|
"description": "Indicates the role this configuration plays (e.g., sensor, controller, etc.)."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"positionVsParent":{
|
||||||
|
"default":"atEquipment",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "atEquipment",
|
||||||
|
"description": "The measurement is taken at the equipment level, typically representing the overall state or performance of the equipment."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "upstream",
|
||||||
|
"description": "The measurement is taken upstream, meaning it is related to inputs or conditions that affect the equipment's operation, such as supply conditions or environmental factors."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "downstream",
|
||||||
|
"description": "The measurement is taken downstream, indicating it relates to outputs or results of the equipment's operation, such as product quality or performance metrics."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"distance":{
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"asset": {
|
"asset": {
|
||||||
@@ -81,6 +109,14 @@
|
|||||||
"description": "Asset tag number which is a universally unique identifier for this asset. May be null if not assigned."
|
"description": "Asset tag number which is a universally unique identifier for this asset. May be null if not assigned."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tagCode":{
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag code which is a unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
"geoLocation": {
|
"geoLocation": {
|
||||||
"default": {
|
"default": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
@@ -122,7 +158,7 @@
|
|||||||
"description": "The supplier or manufacturer of the asset."
|
"description": "The supplier or manufacturer of the asset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"category": {
|
||||||
"default": "sensor",
|
"default": "sensor",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "enum",
|
"type": "enum",
|
||||||
@@ -134,7 +170,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subType": {
|
"type": {
|
||||||
"default": "pressure",
|
"default": "pressure",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -148,6 +184,13 @@
|
|||||||
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit of measurement for this asset (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
"accuracy": {
|
"accuracy": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -70,7 +70,28 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Indicates the role this configuration plays within the system."
|
"description": "Indicates the role this configuration plays within the system."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"positionVsParent":{
|
||||||
|
"default":"atEquipment",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "atEquipment",
|
||||||
|
"description": "The node is connected at the equipment level and is responsible for controlling or monitoring the equipment as a whole."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "upstream",
|
||||||
|
"description": "The node is connected in a downstream position, indicating it is responsible for monitoring or controlling processes that occur after the equipment's operation, such as product flow or output."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "downstream",
|
||||||
|
"description": "The node is connected in an upstream position, indicating it is responsible for monitoring or controlling processes that occur before the equipment's operation, such as input flow or supply."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"asset": {
|
"asset": {
|
||||||
"uuid": {
|
"uuid": {
|
||||||
@@ -81,6 +102,14 @@
|
|||||||
"description": "A universally unique identifier for this asset. May be null if not assigned."
|
"description": "A universally unique identifier for this asset. May be null if not assigned."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tagCode":{
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag code which is a unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
"geoLocation": {
|
"geoLocation": {
|
||||||
"default": {},
|
"default": {},
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -118,14 +147,14 @@
|
|||||||
"description": "The supplier or manufacturer of the asset."
|
"description": "The supplier or manufacturer of the asset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"category": {
|
||||||
"default": "pump",
|
"default": "pump",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subType": {
|
"type": {
|
||||||
"default": "Centrifugal",
|
"default": "Centrifugal",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -139,6 +168,13 @@
|
|||||||
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit of measurement for this asset (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
"accuracy": {
|
"accuracy": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -314,31 +350,6 @@
|
|||||||
"description": "Information about valid command sources recognized by the machine."
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action": {
|
|
||||||
"default": "statusCheck",
|
|
||||||
"rules": {
|
|
||||||
"type": "enum",
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"value": "statusCheck",
|
|
||||||
"description": "Checks the machine's state (mode, submode, operational status)."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "execMovement",
|
|
||||||
"description": "Allows control through auto or GUI setpoints."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "execSequence",
|
|
||||||
"description": "Allows execution of sequences through auto or GUI controls."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "emergencyStop",
|
|
||||||
"description": "Overrides all commands and stops the machine immediately (safety scenarios)."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Defines the possible actions that can be performed on the machine."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sequences":{
|
"sequences":{
|
||||||
"default":{},
|
"default":{},
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -401,6 +412,14 @@
|
|||||||
],
|
],
|
||||||
"description": "The frequency at which calculations are performed."
|
"description": "The frequency at which calculations are performed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"flowNumber": {
|
||||||
|
"default": 1,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": false,
|
||||||
|
"description": "Defines which effluent flow of the parent node to handle."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
387
src/configs/valve.json
Normal file
387
src/configs/valve.json
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "valve",
|
||||||
|
"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": "valve",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type for this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "controller",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Indicates the role this configuration plays within the system."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"positionVsParent":{
|
||||||
|
"default":"atEquipment",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "atEquipment",
|
||||||
|
"description": "The node is connected at the equipment level and is responsible for controlling or monitoring the equipment as a whole."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "upstream",
|
||||||
|
"description": "The node is connected in a downstream position, indicating it is responsible for monitoring or controlling processes that occur after the equipment's operation, such as product flow or output."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "downstream",
|
||||||
|
"description": "The node is connected in an upstream position, indicating it is responsible for monitoring or controlling processes that occur before the equipment's operation, such as input flow or supply."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset": {
|
||||||
|
"uuid": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A universally unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tagCode":{
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag code which is a 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The supplier or manufacturer of the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"default": "valve",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "gate",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A more specific classification within 'type'. For example, 'centrifugal' for a centrifugal pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit of measurement for this asset (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The accuracy of the machine or sensor, typically as a percentage or absolute value."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"valveCurve": {
|
||||||
|
"default": {
|
||||||
|
"1.204": {
|
||||||
|
"1": {
|
||||||
|
"x": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
||||||
|
"y": [0, 18, 50, 95, 150, 216, 337, 564, 882, 1398, 1870]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "valveCurve",
|
||||||
|
"description": "the first parameter is kg (usually according to 1 normal cubic meter per hour acc. to din norm ) and the second parameter is the diameter in mm. The x values are the opening of the valve in percent and the y values are the KV values in m3/h. The KV value is the flow rate of water at a temperature of 20 degrees Celsius through the valve when it is fully open."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"default": "parent",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "parent",
|
||||||
|
"description": "Commands are received from a parent controller."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "GUI",
|
||||||
|
"description": "Commands are received from a graphical user interface."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysical",
|
||||||
|
"description": "Commands are received from physical buttons or switches."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sequences":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"startup": {
|
||||||
|
"default": ["starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for starting up the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shutdown": {
|
||||||
|
"default": ["stopping","coolingdown","idle"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for shutting down the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emergencystop": {
|
||||||
|
"default": ["emergencystop","off"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for an emergency stop."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boot": {
|
||||||
|
"default": ["idle","starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for booting up the machine."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Predefined sequences of states for the machine."
|
||||||
|
|
||||||
|
},
|
||||||
|
"calculationMode": {
|
||||||
|
"default": "medium",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "low",
|
||||||
|
"description": "Calculations run at fixed intervals (time-based)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "medium",
|
||||||
|
"description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "high",
|
||||||
|
"description": "Calculations run on all event-driven info, including every movement."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The frequency at which calculations are performed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
371
src/configs/valveGroupControl.json
Normal file
371
src/configs/valveGroupControl.json
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "ValveGroupControl",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this valveGroupControl configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"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": "valveGroupControl",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type for this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "ValveGroupController",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The supplier or manufacturer of the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "valve",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subType": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A more specific classification within 'type'. For example, 'centrifugal' for a centrifugal pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The accuracy of the valve or sensor, typically as a percentage or absolute value."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "auto",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "auto",
|
||||||
|
"description": "ValveGroupController accepts inputs from a parents and childs 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 valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedActions":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema":{
|
||||||
|
"auto": {
|
||||||
|
"default": ["statusCheck", "execSequence", "emergencyStop", "valvePositionChange", "totalFlowChange", "valveDeltaPchange"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in auto mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"virtualControl": {
|
||||||
|
"default": ["statusCheck", "execSequence", "emergencyStop", "valvePositionChange", "totalFlowChange", "valveDeltaPchange"],
|
||||||
|
"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 valve."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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 valveGroupControl."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"default": "parent",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "parent",
|
||||||
|
"description": "Commands are received from a parent controller."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "GUI",
|
||||||
|
"description": "Commands are received from a graphical user interface."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysical",
|
||||||
|
"description": "Commands are received from physical buttons or switches."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Information about valid command sources recognized by the valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default": "statusCheck",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "statusCheck",
|
||||||
|
"description": "Checks the valveGroupControl's state (mode, submode, operational status)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "valvePositionChange",
|
||||||
|
"description": "If child valve position change, the new flow for each child valve is determined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "execSequence",
|
||||||
|
"description": "Allows execution of sequences through auto or GUI controls."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "totalFlowChange",
|
||||||
|
"description": "If total flow change, the new flow for each child valve is determined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "valveDeltaPchange",
|
||||||
|
"description": "If deltaP change, the deltaPmax is determined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "emergencyStop",
|
||||||
|
"description": "Overrides all commands and stops the valveGroupControl immediately (safety scenarios)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the possible actions that can be performed on the valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sequences":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"startup": {
|
||||||
|
"default": ["starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for starting up the valve."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shutdown": {
|
||||||
|
"default": ["stopping","coolingdown","idle"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for shutting down the valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emergencystop": {
|
||||||
|
"default": ["emergencystop","off"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for an emergency stop."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boot": {
|
||||||
|
"default": ["idle","starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for booting up the valveGroupControl."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Predefined sequences of states for the valveGroupControl."
|
||||||
|
|
||||||
|
},
|
||||||
|
"calculationMode": {
|
||||||
|
"default": "medium",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "low",
|
||||||
|
"description": "Calculations run at fixed intervals (time-based)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "medium",
|
||||||
|
"description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "high",
|
||||||
|
"description": "Calculations run on all event-driven info, including every movement."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The frequency at which calculations are performed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/helper/assertionUtils.js
Normal file
29
src/helper/assertionUtils.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @file assertionUtils.js
|
||||||
|
*
|
||||||
|
* Utility functions for assertions and throwing errors in EVOLV.
|
||||||
|
*
|
||||||
|
* @description This module provides functions to assert conditions and throw errors when those conditions are not met.
|
||||||
|
* @exports ValidationUtils
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Assertions {
|
||||||
|
/**
|
||||||
|
* Assert that no NaN values are present in an array.
|
||||||
|
* @param {Array} arr - The array to check for NaN values.
|
||||||
|
* @param {string} label - Array label to indicate where the error occurs.
|
||||||
|
*/
|
||||||
|
assertNoNaN(arr, label = "array") {
|
||||||
|
if (Array.isArray(arr)) {
|
||||||
|
for (const el of arr) {
|
||||||
|
assertNoNaN(el, label);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Number.isNaN(arr)) {
|
||||||
|
throw new Error(`NaN detected in ${label}!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Assertions;
|
||||||
@@ -1,243 +1,98 @@
|
|||||||
// ChildRegistrationUtils.js
|
|
||||||
class ChildRegistrationUtils {
|
class ChildRegistrationUtils {
|
||||||
constructor(mainClass) {
|
constructor(mainClass) {
|
||||||
this.mainClass = mainClass; // Reference to the main class
|
this.mainClass = mainClass;
|
||||||
this.logger = mainClass.logger;
|
this.logger = mainClass.logger;
|
||||||
|
this.registeredChildren = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerChild(child, positionVsParent) {
|
async registerChild(child, positionVsParent) {
|
||||||
const { softwareType } = child.config.functionality;
|
const { softwareType } = child.config.functionality;
|
||||||
const { name, id, unit } = child.config.general;
|
const { name, id } = child.config.general;
|
||||||
const { type = "", subType = "" } = child.config.asset || {};
|
|
||||||
const emitter = child.emitter;
|
|
||||||
|
|
||||||
//define position vs parent in child
|
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
|
||||||
|
|
||||||
|
// Enhanced child setup - multiple parents
|
||||||
|
if (Array.isArray(child.parent)) {
|
||||||
|
child.parent.push(this.mainClass);
|
||||||
|
} else {
|
||||||
|
child.parent = [this.mainClass];
|
||||||
|
}
|
||||||
child.positionVsParent = positionVsParent;
|
child.positionVsParent = positionVsParent;
|
||||||
child.parent = this.mainClass;
|
|
||||||
|
|
||||||
if (!this.mainClass.child) this.mainClass.child = {};
|
// Enhanced measurement container with rich context
|
||||||
if (!this.mainClass.child[softwareType])
|
if (child.measurements) {
|
||||||
this.mainClass.child[softwareType] = {};
|
child.measurements.setChildId(id);
|
||||||
if (!this.mainClass.child[softwareType][type])
|
child.measurements.setChildName(name);
|
||||||
this.mainClass.child[softwareType][type] = {};
|
child.measurements.setParentRef(this.mainClass);
|
||||||
if (!this.mainClass.child[softwareType][type][subType])
|
|
||||||
this.mainClass.child[softwareType][type][subType] = {};
|
|
||||||
|
|
||||||
// Use an array to handle multiple subtypes
|
|
||||||
if (!Array.isArray(this.mainClass.child[softwareType][type][subType])) {
|
|
||||||
this.mainClass.child[softwareType][type][subType] = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the child in the cloud when available and supply the new child on base of tagcode OLIFANT WE NEED TO FIX THIS SO ITS DYNAMIC!
|
// Store child in your expected structure
|
||||||
/*
|
this._storeChild(child, softwareType);
|
||||||
try{
|
|
||||||
const url = "https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api/asset/create_asset.php?";
|
|
||||||
const TagCode = child.config.asset.tagCode;
|
|
||||||
//console.log(`Register child => ${TagCode}`);
|
|
||||||
const completeURL = url + `asset_product_model_id=1&asset_product_model_uuid=123456789&asset_name=AssetNaam&asset_description=Beschrijving&asset_status=actief&asset_profile_id=1&asset_location_id=1&asset_process_id=11&asset_tag_number=${TagCode}&child_assets=[L6616]`;
|
|
||||||
|
|
||||||
await fetch(completeURL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}catch(e){
|
// Track registration for utilities
|
||||||
console.log("Error saving assetID and tagnumber", e);
|
this.registeredChildren.set(id, {
|
||||||
}*/
|
|
||||||
|
|
||||||
// Push the new child to the array of the mainclass so we can track the childs
|
|
||||||
this.mainClass.child[softwareType][type][subType].push({
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
unit,
|
|
||||||
emitter,
|
|
||||||
});
|
|
||||||
|
|
||||||
//then connect the child depending on the type subtype etc..
|
|
||||||
this.connectChild(
|
|
||||||
id,
|
|
||||||
softwareType,
|
|
||||||
emitter,
|
|
||||||
type,
|
|
||||||
child,
|
child,
|
||||||
subType,
|
softwareType,
|
||||||
positionVsParent
|
position: positionVsParent,
|
||||||
);
|
registeredAt: Date.now()
|
||||||
}
|
|
||||||
|
|
||||||
connectChild(
|
|
||||||
id,
|
|
||||||
softwareType,
|
|
||||||
emitter,
|
|
||||||
type,
|
|
||||||
child,
|
|
||||||
subType,
|
|
||||||
positionVsParent
|
|
||||||
) {
|
|
||||||
this.logger.debug(
|
|
||||||
`Connecting child id=${id}: desc=${softwareType}, type=${type},subType=${subType}, position=${positionVsParent}`
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (softwareType) {
|
|
||||||
case "measurement":
|
|
||||||
this.logger.debug(
|
|
||||||
`Registering measurement child: ${id} with type=${type}`
|
|
||||||
);
|
|
||||||
this.connectMeasurement(child, subType, positionVsParent);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "machine":
|
|
||||||
this.logger.debug(`Registering complete machine child: ${id}`);
|
|
||||||
this.connectMachine(child);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "valve":
|
|
||||||
this.logger.debug(`Registering complete valve child: ${id}`);
|
|
||||||
this.connectValve(child);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "actuator":
|
|
||||||
this.logger.debug(`Registering linear actuator child: ${id}`);
|
|
||||||
this.connectActuator(child,positionVsParent);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.logger.error(`Child registration unrecognized desc: ${desc}`);
|
|
||||||
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectMeasurement(child, subType, position) {
|
|
||||||
this.logger.debug(
|
|
||||||
`Connecting measurement child: ${subType} with position=${position}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if subType is valid
|
|
||||||
if (!subType) {
|
|
||||||
this.logger.error(`Invalid subType for measurement: ${subType}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the measurement to a number - logging each step for debugging
|
|
||||||
try {
|
|
||||||
this.logger.debug(
|
|
||||||
`Initializing measurement: ${subType}, position: ${position} value: 0`
|
|
||||||
);
|
|
||||||
const typeResult = this.mainClass.measurements.type(subType);
|
|
||||||
const variantResult = typeResult.variant("measured");
|
|
||||||
const positionResult = variantResult.position(position);
|
|
||||||
positionResult.value(0);
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Subscribing on mAbs event for measurement: ${subType}, position: ${position}`
|
|
||||||
);
|
|
||||||
// Listen for the mAbs event and update the measurement
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Successfully initialized measurement: ${subType}, position: ${position}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to initialize measurement: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
child.emitter.on("mAbs", (value) => {
|
|
||||||
// Use the same method chaining approach that worked during initialization
|
|
||||||
this.mainClass.measurements
|
|
||||||
.type(subType)
|
|
||||||
.variant("measured")
|
|
||||||
.position(position)
|
|
||||||
.value(value);
|
|
||||||
this.mainClass.updateMeasurement("measured", subType, value, position);
|
|
||||||
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${subType}, value: ${value}, position: ${position}`);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
connectMachine(machine) {
|
// IMPORTANT: Only call parent registration - no automatic handling and if parent has this function then try to register this child
|
||||||
if (!machine) {
|
if (typeof this.mainClass.registerChild === 'function') {
|
||||||
this.logger.error("Invalid machine provided.");
|
this.mainClass.registerChild(child, softwareType);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const machineId = Object.keys(this.mainClass.machines).length + 1;
|
this.logger.info(`✅ Child ${name} registered successfully`);
|
||||||
this.mainClass.machines[machineId] = machine;
|
|
||||||
|
|
||||||
this.logger.info(
|
|
||||||
`Setting up pressureChange listener for machine ${machineId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
machine.emitter.on("pressureChange", () =>
|
|
||||||
this.mainClass.handlePressureChange(machine)
|
|
||||||
);
|
|
||||||
|
|
||||||
//update of child triggers the handler
|
|
||||||
this.mainClass.handleChildChange();
|
|
||||||
|
|
||||||
this.logger.info(`Machine ${machineId} registered successfully.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectValve(valve) {
|
_storeChild(child, softwareType) {
|
||||||
if (!valve) {
|
// Maintain your existing structure
|
||||||
this.logger.warn("Invalid valve provided.");
|
if (!this.mainClass.child) this.mainClass.child = {};
|
||||||
return;
|
if (!this.mainClass.child[softwareType]) this.mainClass.child[softwareType] = {};
|
||||||
|
|
||||||
|
const { category = "sensor" } = child.config.asset || {};
|
||||||
|
if (!this.mainClass.child[softwareType][category]) {
|
||||||
|
this.mainClass.child[softwareType][category] = [];
|
||||||
}
|
}
|
||||||
const valveId = Object.keys(this.mainClass.valves).length + 1;
|
|
||||||
this.mainClass.valves[valveId] = valve; // Gooit valve object in de valves attribute met valve objects
|
this.mainClass.child[softwareType][category].push(child);
|
||||||
|
|
||||||
valve.state.emitter.on("positionChange", (data) => {
|
|
||||||
//ValveGroupController abboneren op klepstand verandering
|
|
||||||
this.mainClass.logger.debug(`Position change of valve detected: ${data}`);
|
|
||||||
this.mainClass.calcValveFlows();
|
|
||||||
}); //bepaal nieuwe flow per valve
|
|
||||||
valve.emitter.on("deltaPChange", () => {
|
|
||||||
this.mainClass.logger.debug("DeltaP change of valve detected");
|
|
||||||
this.mainClass.calcMaxDeltaP();
|
|
||||||
}); //bepaal nieuwe max deltaP
|
|
||||||
|
|
||||||
this.logger.info(`Valve ${valveId} registered successfully.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectActuator(actuator, positionVsParent) {
|
// NEW: Utility methods for parent to use
|
||||||
if (!actuator) {
|
getChildrenOfType(softwareType, category = null) {
|
||||||
this.logger.warn("Invalid actuator provided.");
|
if (!this.mainClass.child[softwareType]) return [];
|
||||||
return;
|
|
||||||
}
|
if (category) {
|
||||||
|
return this.mainClass.child[softwareType][category] || [];
|
||||||
//Special case gateGroupControl
|
|
||||||
if (
|
|
||||||
this.mainClass.config.functionality.softwareType == "gateGroupControl"
|
|
||||||
) {
|
|
||||||
if (Object.keys(this.mainClass.actuators).length < 2) {
|
|
||||||
if (positionVsParent == "downstream") {
|
|
||||||
this.mainClass.actuators[0] = actuator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positionVsParent == "upstream") {
|
|
||||||
this.mainClass.actuators[1] = actuator;
|
|
||||||
}
|
|
||||||
//define emitters
|
|
||||||
actuator.state.emitter.on("positionChange", (data) => {
|
|
||||||
this.mainClass.logger.debug(`Position change of actuator detected: ${data}`);
|
|
||||||
this.mainClass.eventUpdate();
|
|
||||||
});
|
|
||||||
|
|
||||||
//define emitters
|
|
||||||
actuator.state.emitter.on("stateChange", (data) => {
|
|
||||||
this.mainClass.logger.debug(`State change of actuator detected: ${data}`);
|
|
||||||
this.mainClass.eventUpdate();
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.logger.error(
|
|
||||||
"Too many actuators registered. Only two are allowed."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return all children of this software type
|
||||||
|
return Object.values(this.mainClass.child[softwareType]).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
//wanneer hij deze ontvangt is deltaP van een van de valves veranderd (kan ook zijn niet child zijn, maar dat maakt niet uit)
|
getChildById(childId) {
|
||||||
|
return this.registeredChildren.get(childId)?.child || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllChildren() {
|
||||||
|
return Array.from(this.registeredChildren.values()).map(r => r.child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Debugging utilities
|
||||||
|
logChildStructure() {
|
||||||
|
this.logger.debug('Current child structure:', JSON.stringify(
|
||||||
|
Object.keys(this.mainClass.child).reduce((acc, softwareType) => {
|
||||||
|
acc[softwareType] = Object.keys(this.mainClass.child[softwareType]).reduce((catAcc, category) => {
|
||||||
|
catAcc[category] = this.mainClass.child[softwareType][category].map(c => ({
|
||||||
|
id: c.config.general.id,
|
||||||
|
name: c.config.general.name
|
||||||
|
}));
|
||||||
|
return catAcc;
|
||||||
|
}, {});
|
||||||
|
return acc;
|
||||||
|
}, {}), null, 2
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ChildRegistrationUtils;
|
module.exports = ChildRegistrationUtils;
|
||||||
260
src/helper/childRegistrationUtils_DEPRECATED.js
Normal file
260
src/helper/childRegistrationUtils_DEPRECATED.js
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
// ChildRegistrationUtils.js
|
||||||
|
class ChildRegistrationUtils {
|
||||||
|
constructor(mainClass) {
|
||||||
|
this.mainClass = mainClass; // Reference to the main class
|
||||||
|
this.logger = mainClass.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerChild(child, positionVsParent) {
|
||||||
|
|
||||||
|
this.logger.debug(`Registering child: ${child.id} with position=${positionVsParent}`);
|
||||||
|
const { softwareType } = child.config.functionality;
|
||||||
|
const { name, id, unit } = child.config.general;
|
||||||
|
const { category = "", type = "" } = child.config.asset || {};
|
||||||
|
console.log(`Registering child: ${name}, id: ${id}, softwareType: ${softwareType}, category: ${category}, type: ${type}, positionVsParent: ${positionVsParent}` );
|
||||||
|
const emitter = child.emitter;
|
||||||
|
|
||||||
|
//define position vs parent in child
|
||||||
|
child.positionVsParent = positionVsParent;
|
||||||
|
child.parent = this.mainClass;
|
||||||
|
|
||||||
|
if (!this.mainClass.child) this.mainClass.child = {};
|
||||||
|
if (!this.mainClass.child[softwareType])
|
||||||
|
this.mainClass.child[softwareType] = {};
|
||||||
|
if (!this.mainClass.child[softwareType][category])
|
||||||
|
this.mainClass.child[softwareType][category] = {};
|
||||||
|
if (!this.mainClass.child[softwareType][category][type])
|
||||||
|
this.mainClass.child[softwareType][category][type] = {};
|
||||||
|
|
||||||
|
// Use an array to handle multiple categories
|
||||||
|
if (!Array.isArray(this.mainClass.child[softwareType][category][type])) {
|
||||||
|
this.mainClass.child[softwareType][category][type] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the new child to the array of the mainclass so we can track the childs
|
||||||
|
this.mainClass.child[softwareType][category][type].push({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
unit,
|
||||||
|
emitter,
|
||||||
|
});
|
||||||
|
|
||||||
|
//then connect the child depending on the type type etc..
|
||||||
|
this.connectChild(
|
||||||
|
id,
|
||||||
|
softwareType,
|
||||||
|
emitter,
|
||||||
|
category,
|
||||||
|
child,
|
||||||
|
type,
|
||||||
|
positionVsParent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectChild(
|
||||||
|
id,
|
||||||
|
softwareType,
|
||||||
|
emitter,
|
||||||
|
category,
|
||||||
|
child,
|
||||||
|
type,
|
||||||
|
positionVsParent
|
||||||
|
) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Connecting child id=${id}: desc=${softwareType}, category=${category},type=${type}, position=${positionVsParent}`
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (softwareType) {
|
||||||
|
case "measurement":
|
||||||
|
this.logger.debug(
|
||||||
|
`Registering measurement child: ${id} with category=${category}`
|
||||||
|
);
|
||||||
|
this.connectMeasurement(child, type, positionVsParent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "machine":
|
||||||
|
this.logger.debug(`Registering complete machine child: ${id}`);
|
||||||
|
this.connectMachine(child);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "valve":
|
||||||
|
this.logger.debug(`Registering complete valve child: ${id}`);
|
||||||
|
this.connectValve(child);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "machineGroup":
|
||||||
|
this.logger.debug(`Registering complete machineGroup child: ${id}`);
|
||||||
|
this.connectMachineGroup(child);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "actuator":
|
||||||
|
this.logger.debug(`Registering linear actuator child: ${id}`);
|
||||||
|
this.connectActuator(child,positionVsParent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.logger.error(`Child registration unrecognized desc: ${desc}`);
|
||||||
|
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectMeasurement(child, type, position) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Connecting measurement child: ${type} with position=${position}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if type is valid
|
||||||
|
if (!type) {
|
||||||
|
this.logger.error(`Invalid type for measurement: ${type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the measurement to a number - logging each step for debugging
|
||||||
|
try {
|
||||||
|
this.logger.debug(
|
||||||
|
`Initializing measurement: ${type}, position: ${position} value: 0`
|
||||||
|
);
|
||||||
|
const typeResult = this.mainClass.measurements.type(type);
|
||||||
|
const variantResult = typeResult.variant("measured");
|
||||||
|
const positionResult = variantResult.position(position);
|
||||||
|
positionResult.value(0);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Subscribing on mAbs event for measurement: ${type}, position: ${position}`
|
||||||
|
);
|
||||||
|
// Listen for the mAbs event and update the measurement
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Successfully initialized measurement: ${type}, position: ${position}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to initialize measurement: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//testing new emitter strategy
|
||||||
|
child.measurements.emitter.on("newValue", (data) => {
|
||||||
|
this.logger.warn(
|
||||||
|
`Value change event received for measurement: ${type}, position: ${position}, value: ${data.value}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.emitter.on("mAbs", (value) => {
|
||||||
|
// Use the same method chaining approach that worked during initialization
|
||||||
|
this.mainClass.measurements
|
||||||
|
.type(type)
|
||||||
|
.variant("measured")
|
||||||
|
.position(position)
|
||||||
|
.value(value);
|
||||||
|
this.mainClass.updateMeasurement("measured", type, value, position);
|
||||||
|
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${type}, value: ${value}, position: ${position}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connectMachine(machine) {
|
||||||
|
if (!machine) {
|
||||||
|
this.logger.error("Invalid machine provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const machineId = Object.keys(this.mainClass.machines).length + 1;
|
||||||
|
this.mainClass.machines[machineId] = machine;
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`Setting up pressureChange listener for machine ${machineId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
machine.emitter.on("pressureChange", () =>
|
||||||
|
this.mainClass.handlePressureChange(machine)
|
||||||
|
);
|
||||||
|
|
||||||
|
//update of child triggers the handler
|
||||||
|
this.mainClass.handleChildChange();
|
||||||
|
|
||||||
|
this.logger.info(`Machine ${machineId} registered successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectValve(valve) {
|
||||||
|
if (!valve) {
|
||||||
|
this.logger.warn("Invalid valve provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valveId = Object.keys(this.mainClass.valves).length + 1;
|
||||||
|
this.mainClass.valves[valveId] = valve; // Gooit valve object in de valves attribute met valve objects
|
||||||
|
|
||||||
|
valve.state.emitter.on("positionChange", (data) => {
|
||||||
|
//ValveGroupController abboneren op klepstand verandering
|
||||||
|
this.mainClass.logger.debug(`Position change of valve detected: ${data}`);
|
||||||
|
this.mainClass.calcValveFlows();
|
||||||
|
}); //bepaal nieuwe flow per valve
|
||||||
|
valve.emitter.on("deltaPChange", () => {
|
||||||
|
this.mainClass.logger.debug("DeltaP change of valve detected");
|
||||||
|
this.mainClass.calcMaxDeltaP();
|
||||||
|
}); //bepaal nieuwe max deltaP
|
||||||
|
|
||||||
|
this.logger.info(`Valve ${valveId} registered successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectMachineGroup(machineGroup) {
|
||||||
|
if (!machineGroup) {
|
||||||
|
this.logger.warn("Invalid machineGroup provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const machineGroupId = Object.keys(this.mainClass.machineGroups).length + 1;
|
||||||
|
this.mainClass.machineGroups[machineGroupId] = machineGroup;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Skip machinegroup connnection: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
machineGroup.emitter.on("totalFlowChange", (data) => {
|
||||||
|
this.mainClass.logger.debug('Total flow change of machineGroup detected');
|
||||||
|
this.mainClass.handleInput("parent", "totalFlowChange", data)}); //Geef nieuwe totale flow door aan valveGrouControl
|
||||||
|
|
||||||
|
this.logger.info(`MachineGroup ${machineGroup.config.general.name} registered successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectActuator(actuator, positionVsParent) {
|
||||||
|
if (!actuator) {
|
||||||
|
this.logger.warn("Invalid actuator provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Special case gateGroupControl
|
||||||
|
if (
|
||||||
|
this.mainClass.config.functionality.softwareType == "gateGroupControl"
|
||||||
|
) {
|
||||||
|
if (Object.keys(this.mainClass.actuators).length < 2) {
|
||||||
|
if (positionVsParent == "downstream") {
|
||||||
|
this.mainClass.actuators[0] = actuator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionVsParent == "upstream") {
|
||||||
|
this.mainClass.actuators[1] = actuator;
|
||||||
|
}
|
||||||
|
//define emitters
|
||||||
|
actuator.state.emitter.on("positionChange", (data) => {
|
||||||
|
this.mainClass.logger.debug(`Position change of actuator detected: ${data}`);
|
||||||
|
this.mainClass.eventUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
//define emitters
|
||||||
|
actuator.state.emitter.on("stateChange", (data) => {
|
||||||
|
this.mainClass.logger.debug(`State change of actuator detected: ${data}`);
|
||||||
|
this.mainClass.eventUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.logger.error(
|
||||||
|
"Too many actuators registered. Only two are allowed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//wanneer hij deze ontvangt is deltaP van een van de valves veranderd (kan ook zijn niet child zijn, maar dat maakt niet uit)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ChildRegistrationUtils;
|
||||||
106
src/helper/endpointUtils.js
Normal file
106
src/helper/endpointUtils.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
const MenuUtils = require('./menuUtils');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side helper for exposing MenuUtils to the browser via HTTP endpoints.
|
||||||
|
*/
|
||||||
|
class EndpointUtils {
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Function} options.MenuUtilsClass the MenuUtils constructor/function
|
||||||
|
*/
|
||||||
|
constructor({ MenuUtilsClass = MenuUtils } = {}) {
|
||||||
|
this.MenuUtils = MenuUtilsClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an HTTP GET endpoint that serves the client-side MenuUtils code
|
||||||
|
* @param {object} RED the Node-RED API object
|
||||||
|
* @param {string} nodeName the name of the node (used in the URL)
|
||||||
|
* @param {object} customHelpers additional helper functions to inject
|
||||||
|
*/
|
||||||
|
createMenuUtilsEndpoint(RED, nodeName, customHelpers = {}) {
|
||||||
|
RED.httpAdmin.get(`/${nodeName}/resources/menuUtils.js`, (req, res) => {
|
||||||
|
console.log(`Serving menuUtils.js for ${nodeName} node`);
|
||||||
|
res.set('Content-Type', 'application/javascript');
|
||||||
|
const browserCode = this.generateMenuUtilsCode(nodeName, customHelpers);
|
||||||
|
res.send(browserCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the browser-side JavaScript that redefines MenuUtils and helper fns
|
||||||
|
* @param {string} nodeName
|
||||||
|
* @param {object} customHelpers map of name: functionString pairs
|
||||||
|
* @returns {string} a JS snippet to run in the browser
|
||||||
|
*/
|
||||||
|
generateMenuUtilsCode(nodeName, customHelpers = {}) {
|
||||||
|
// Default helper implementations to expose alongside MenuUtils
|
||||||
|
const defaultHelpers = {
|
||||||
|
validateRequired: `function(value) {
|
||||||
|
return value != null && value.toString().trim() !== '';
|
||||||
|
}`,
|
||||||
|
formatDisplayValue: `function(value, unit) {
|
||||||
|
return \`${'${'}value} ${'${'}unit || ''}\`.trim();
|
||||||
|
}`,
|
||||||
|
validateScaling: `function(min, max) {
|
||||||
|
return !isNaN(min) && !isNaN(max) && Number(min) < Number(max);
|
||||||
|
}`,
|
||||||
|
validateUnit: `function(unit) {
|
||||||
|
return typeof unit === 'string' && unit.trim() !== '';
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge any custom overrides
|
||||||
|
const allHelpers = { ...defaultHelpers, ...customHelpers };
|
||||||
|
|
||||||
|
// Build the helpers code block
|
||||||
|
const helpersCode = Object.entries(allHelpers)
|
||||||
|
.map(([name, fnBody]) => ` ${name}: ${fnBody}`)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
// Introspect MenuUtils prototype to extract method definitions
|
||||||
|
const proto = this.MenuUtils.prototype;
|
||||||
|
const browserMethods = Object.getOwnPropertyNames(proto)
|
||||||
|
.filter(key => key !== 'constructor')
|
||||||
|
.map(methodName => {
|
||||||
|
const fn = proto[methodName];
|
||||||
|
const src = fn.toString();
|
||||||
|
const isAsync = fn.constructor.name === 'AsyncFunction';
|
||||||
|
// extract signature and body
|
||||||
|
const signature = src.slice(src.indexOf('('));
|
||||||
|
const prefix = isAsync ? 'async ' : '';
|
||||||
|
return ` ${prefix}${methodName}${signature}`;
|
||||||
|
})
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
// Return a complete JS snippet for the browser
|
||||||
|
return `
|
||||||
|
// Auto-generated MenuUtils for node: ${nodeName}
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
|
||||||
|
class MenuUtils {
|
||||||
|
constructor(opts) {
|
||||||
|
// Allow same options API as server-side
|
||||||
|
this.useCloud = opts.useCloud || false;
|
||||||
|
this.projectSettings = opts.projectSettings || {};
|
||||||
|
// any other client-side initialization...
|
||||||
|
}
|
||||||
|
|
||||||
|
${browserMethods}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.EVOLV.nodes.${nodeName}.utils = {
|
||||||
|
menuUtils: new MenuUtils({}),
|
||||||
|
helpers: {
|
||||||
|
${helpersCode}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Loaded EVOLV.nodes.${nodeName}.utils.menuUtils');
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EndpointUtils;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
class MenuUtils {
|
class MenuUtils {
|
||||||
|
|
||||||
|
|
||||||
initBasicToggles(elements) {
|
initBasicToggles(elements) {
|
||||||
// Toggle visibility for log level
|
// Toggle visibility for log level
|
||||||
|
|||||||
543
src/helper/menuUtils_DEPRECATED.js
Normal file
543
src/helper/menuUtils_DEPRECATED.js
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
class MenuUtils {
|
||||||
|
|
||||||
|
|
||||||
|
initBasicToggles(elements) {
|
||||||
|
// Toggle visibility for log level
|
||||||
|
elements.logCheckbox.addEventListener("change", function () {
|
||||||
|
elements.rowLogLevel.style.display = this.checked ? "block" : "none";
|
||||||
|
});
|
||||||
|
elements.rowLogLevel.style.display = elements.logCheckbox.checked
|
||||||
|
? "block"
|
||||||
|
: "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the initialize toggles function within scope
|
||||||
|
initMeasurementToggles(elements) {
|
||||||
|
// Toggle visibility for scaling inputs
|
||||||
|
elements.scalingCheckbox.addEventListener("change", function () {
|
||||||
|
elements.rowInputMin.style.display = this.checked ? "block" : "none";
|
||||||
|
elements.rowInputMax.style.display = this.checked ? "block" : "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set initial states
|
||||||
|
elements.rowInputMin.style.display = elements.scalingCheckbox.checked
|
||||||
|
? "block"
|
||||||
|
: "none";
|
||||||
|
elements.rowInputMax.style.display = elements.scalingCheckbox.checked
|
||||||
|
? "block"
|
||||||
|
: "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
initTensionToggles(elements, node) {
|
||||||
|
const currentMethod = node.interpolationMethod;
|
||||||
|
elements.rowTension.style.display =
|
||||||
|
currentMethod === "monotone_cubic_spline" ? "block" : "none";
|
||||||
|
console.log(
|
||||||
|
"Initial tension row display: ",
|
||||||
|
elements.rowTension.style.display
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.interpolationMethodInput.addEventListener("change", function () {
|
||||||
|
const selectedMethod = this.value;
|
||||||
|
console.log(`Interpolation method changed: ${selectedMethod}`);
|
||||||
|
node.interpolationMethod = selectedMethod;
|
||||||
|
|
||||||
|
// Toggle visibility for tension input
|
||||||
|
elements.rowTension.style.display =
|
||||||
|
selectedMethod === "monotone_cubic_spline" ? "block" : "none";
|
||||||
|
console.log("Tension row display: ", elements.rowTension.style.display);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Define the smoothing methods population function within scope
|
||||||
|
populateSmoothingMethods(configUrls, elements, node) {
|
||||||
|
this.fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const smoothingMethods =
|
||||||
|
configData.smoothing?.smoothMethod?.rules?.values?.map(
|
||||||
|
(o) => o.value
|
||||||
|
) || [];
|
||||||
|
this.populateDropdown(
|
||||||
|
elements.smoothMethod,
|
||||||
|
smoothingMethods,
|
||||||
|
node,
|
||||||
|
"smooth_method"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error loading smoothing methods", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
populateInterpolationMethods(configUrls, elements, node) {
|
||||||
|
this.fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const interpolationMethods =
|
||||||
|
configData?.interpolation?.type?.rules?.values.map((m) => m.value) ||
|
||||||
|
[];
|
||||||
|
this.populateDropdown(
|
||||||
|
elements.interpolationMethodInput,
|
||||||
|
interpolationMethods,
|
||||||
|
node,
|
||||||
|
"interpolationMethod"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the selected method and use it to spawn 1 more field to fill in tension
|
||||||
|
//const selectedMethod = interpolationMethods.find(m => m === node.interpolationMethod);
|
||||||
|
this.initTensionToggles(elements, node);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error loading interpolation methods", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
populateLogLevelOptions(logLevelSelect, configData, node) {
|
||||||
|
// debug log level
|
||||||
|
//console.log("Displaying configData => ", configData) ;
|
||||||
|
|
||||||
|
const logLevels =
|
||||||
|
configData?.general?.logging?.logLevel?.rules?.values?.map(
|
||||||
|
(l) => l.value
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
//console.log("Displaying logLevels => ", logLevels);
|
||||||
|
|
||||||
|
// Reuse your existing generic populateDropdown helper
|
||||||
|
this.populateDropdown(logLevelSelect, logLevels, node.logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
//cascade dropdowns for asset type, supplier, subType, model, unit
|
||||||
|
fetchAndPopulateDropdowns(configUrls, elements, node) {
|
||||||
|
this.fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const assetType = configData.asset?.type?.default;
|
||||||
|
const localSuppliersUrl = this.constructUrl(configUrls.local.taggcodeAPI,`${assetType}s`,"suppliers.json");
|
||||||
|
const cloudSuppliersUrl = this.constructCloudURL(configUrls.cloud.taggcodeAPI, "/vendor/get_vendors.php");
|
||||||
|
|
||||||
|
return this.fetchData(cloudSuppliersUrl, localSuppliersUrl)
|
||||||
|
.then((supplierData) => {
|
||||||
|
|
||||||
|
const suppliers = supplierData.map((supplier) => supplier.name);
|
||||||
|
|
||||||
|
// Populate suppliers dropdown and set up its change handler
|
||||||
|
return this.populateDropdown(
|
||||||
|
elements.supplier,
|
||||||
|
suppliers,
|
||||||
|
node,
|
||||||
|
"supplier",
|
||||||
|
function (selectedSupplier) {
|
||||||
|
if (selectedSupplier) {
|
||||||
|
this.populateSubTypes(configUrls, elements, node, selectedSupplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// If we have a saved supplier, trigger subTypes population
|
||||||
|
if (node.supplier) {
|
||||||
|
this.populateSubTypes(configUrls, elements, node, node.supplier);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error in initial dropdown population:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpecificConfigUrl(nodeName,cloudAPI) {
|
||||||
|
|
||||||
|
const cloudConfigURL = cloudAPI + "/config/" + nodeName + ".json";
|
||||||
|
const localConfigURL = "http://localhost:1880/"+ nodeName + "/dependencies/"+ nodeName + "/" + nodeName + "Config.json";
|
||||||
|
|
||||||
|
return { cloudConfigURL, localConfigURL };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes to API
|
||||||
|
async apiCall(node) {
|
||||||
|
try{
|
||||||
|
// OLFIANT when a browser refreshes the tag code is lost!!! fix this later!!!!!
|
||||||
|
// FIX UUID ALSO LATER
|
||||||
|
|
||||||
|
if(node.assetTagCode !== "" || node.assetTagCode !== null){ }
|
||||||
|
// API call to register or check asset in central database
|
||||||
|
let assetregisterAPI = node.configUrls.cloud.taggcodeAPI + "/asset/create_asset.php";
|
||||||
|
|
||||||
|
const assetModelId = node.modelMetadata.id; //asset_product_model_id
|
||||||
|
const uuid = node.uuid; //asset_product_model_uuid
|
||||||
|
const assetName = node.assetType; //asset_name / type?
|
||||||
|
const description = node.name; // asset_description
|
||||||
|
const assetStatus = "actief"; //asset_status -> koppel aan enable / disable node ? or make dropdown ?
|
||||||
|
const assetProfileId = 1; //asset_profile_id these are the rules to check if the childs are valid under this node (parent / child id?)
|
||||||
|
const child_assets = ["63247"]; //child_assets tagnummer of id?
|
||||||
|
const assetProcessId = node.processId; //asset_process_id
|
||||||
|
const assetLocationId = node.locationId; //asset_location_id
|
||||||
|
const tagCode = node.assetTagCode; // if already exists in the node information use it to tell the api it exists and it will update else we will get it from the api call
|
||||||
|
//console.log(`this is my tagCode: ${tagCode}`);
|
||||||
|
|
||||||
|
// Build base URL with required parameters
|
||||||
|
let apiUrl = `?asset_product_model_id=${assetModelId}&asset_product_model_uuid=${uuid}&asset_name=${assetName}&asset_description=${description}&asset_status=${assetStatus}&asset_profile_id=${assetProfileId}&asset_location_id=${assetLocationId}&asset_process_id=${assetProcessId}&child_assets=${child_assets}`;
|
||||||
|
|
||||||
|
// Only add tagCode to URL if it exists
|
||||||
|
if (tagCode) {
|
||||||
|
apiUrl += `&asset_tag_number=${tagCode}`;
|
||||||
|
console.log('hello there');
|
||||||
|
}
|
||||||
|
|
||||||
|
assetregisterAPI += apiUrl;
|
||||||
|
console.log("API call to register asset in central database", assetregisterAPI);
|
||||||
|
|
||||||
|
const response = await fetch(assetregisterAPI, {
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the response text first
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log("Raw API response:", responseText);
|
||||||
|
|
||||||
|
// Try to parse the JSON, handling potential parsing errors
|
||||||
|
let jsonResponse;
|
||||||
|
try {
|
||||||
|
jsonResponse = JSON.parse(responseText);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("JSON Parsing Error:", parseError);
|
||||||
|
console.error("Response that could not be parsed:", responseText);
|
||||||
|
throw new Error("Failed to parse API response");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(jsonResponse);
|
||||||
|
|
||||||
|
if(jsonResponse.success){
|
||||||
|
console.log(`${jsonResponse.message}, tag number: ${jsonResponse.asset_tag_number}, asset id: ${jsonResponse.asset_id}`);
|
||||||
|
// Save the asset tag number and id to the node
|
||||||
|
} else {
|
||||||
|
console.log("Asset not registered in central database");
|
||||||
|
}
|
||||||
|
return jsonResponse;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error saving changes to asset register API", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fetchData(url, fallbackUrl) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
const responsData = await response.json();
|
||||||
|
//responsData
|
||||||
|
const data = responsData.data;
|
||||||
|
/* .map(item => {
|
||||||
|
const { vendor_name, ...rest } = item;
|
||||||
|
return {
|
||||||
|
name: vendor_name,
|
||||||
|
...rest
|
||||||
|
};
|
||||||
|
}); */
|
||||||
|
console.log(url);
|
||||||
|
console.log("Response Data: ", data);
|
||||||
|
return data;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`Primary URL failed: ${url}. Trying fallback URL: ${fallbackUrl}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const response = await fetch(fallbackUrl);
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
console.error("Both primary and fallback URLs failed:", fallbackErr);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchProjectData(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
const responsData = await response.json();
|
||||||
|
console.log("Response Data: ", responsData);
|
||||||
|
return responsData;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async populateDropdown(
|
||||||
|
htmlElement,
|
||||||
|
options,
|
||||||
|
node,
|
||||||
|
property,
|
||||||
|
callback
|
||||||
|
) {
|
||||||
|
this.generateHtml(htmlElement, options, node[property]);
|
||||||
|
|
||||||
|
htmlElement.addEventListener("change", async (e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
console.log(`Dropdown changed: ${property} = ${newValue}`);
|
||||||
|
node[property] = newValue;
|
||||||
|
|
||||||
|
RED.nodes.dirty(true);
|
||||||
|
if (callback) await callback(newValue); // Ensure async callback completion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to construct a URL from a base and path internal
|
||||||
|
constructUrl(base, ...paths) {
|
||||||
|
|
||||||
|
// Remove trailing slash from base and leading slashes from paths
|
||||||
|
const sanitizedBase = (base || "").replace(/\/+$/, "");
|
||||||
|
const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));
|
||||||
|
|
||||||
|
// Join sanitized base and paths
|
||||||
|
const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
|
||||||
|
console.log("Base:", sanitizedBase);
|
||||||
|
console.log("Paths:", sanitizedPaths);
|
||||||
|
console.log("Constructed URL:", url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Adjust for API Gateway
|
||||||
|
constructCloudURL(base, ...paths) {
|
||||||
|
// Remove trailing slash from base and leading slashes from paths
|
||||||
|
const sanitizedBase = base.replace(/\/+$/, "");
|
||||||
|
const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));
|
||||||
|
// Join sanitized base and paths
|
||||||
|
const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateSubTypes(configUrls, elements, node, selectedSupplier) {
|
||||||
|
|
||||||
|
this.fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const assetType = configData.asset?.type?.default;
|
||||||
|
const supplierFolder = this.constructUrl( configUrls.local.taggcodeAPI, `${assetType}s`, selectedSupplier );
|
||||||
|
|
||||||
|
const localSubTypesUrl = this.constructUrl(supplierFolder, "subtypes.json");
|
||||||
|
const cloudSubTypesUrl = this.constructCloudURL(configUrls.cloud.taggcodeAPI, "/product/get_subtypesFromVendor.php?vendor_name=" + selectedSupplier);
|
||||||
|
|
||||||
|
return this.fetchData(cloudSubTypesUrl, localSubTypesUrl)
|
||||||
|
.then((subTypeData) => {
|
||||||
|
const subTypes = subTypeData.map((subType) => subType.name);
|
||||||
|
|
||||||
|
return this.populateDropdown(
|
||||||
|
elements.subType,
|
||||||
|
subTypes,
|
||||||
|
node,
|
||||||
|
"subType",
|
||||||
|
function (selectedSubType) {
|
||||||
|
if (selectedSubType) {
|
||||||
|
// When subType changes, update both models and units
|
||||||
|
this.populateModels(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSupplier,
|
||||||
|
selectedSubType
|
||||||
|
);
|
||||||
|
this.populateUnitsForSubType(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSubType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// If we have a saved subType, trigger both models and units population
|
||||||
|
if (node.subType) {
|
||||||
|
this.populateModels(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSupplier,
|
||||||
|
node.subType
|
||||||
|
);
|
||||||
|
this.populateUnitsForSubType(configUrls, elements, node, node.subType);
|
||||||
|
}
|
||||||
|
//console.log("In fetch part of subtypes ");
|
||||||
|
// Store all data from selected model
|
||||||
|
/* node["modelMetadata"] = modelData.find(
|
||||||
|
(model) => model.name === node.model
|
||||||
|
);
|
||||||
|
console.log("Model Metadata: ", node["modelMetadata"]); */
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error populating subtypes:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
populateUnitsForSubType(configUrls, elements, node, selectedSubType) {
|
||||||
|
// Fetch the units data
|
||||||
|
this.fetchData(configUrls.cloud.units, configUrls.local.units)
|
||||||
|
.then((unitsData) => {
|
||||||
|
// Find the category that matches the subType name
|
||||||
|
const categoryData = unitsData.units.find(
|
||||||
|
(category) =>
|
||||||
|
category.category.toLowerCase() === selectedSubType.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categoryData) {
|
||||||
|
// Extract just the unit values and descriptions
|
||||||
|
const units = categoryData.values.map((unit) => ({
|
||||||
|
value: unit.value,
|
||||||
|
description: unit.description,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create the options array with descriptions as labels
|
||||||
|
const options = units.map((unit) => ({
|
||||||
|
value: unit.value,
|
||||||
|
label: `${unit.value} - ${unit.description}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Populate the units dropdown
|
||||||
|
this.populateDropdown(
|
||||||
|
elements.unit,
|
||||||
|
options.map((opt) => opt.value),
|
||||||
|
node,
|
||||||
|
"unit"
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there's no currently selected unit but we have options, select the first one
|
||||||
|
if (!node.unit && options.length > 0) {
|
||||||
|
node.unit = options[0].value;
|
||||||
|
elements.unit.value = options[0].value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no matching category is found, provide a default % option
|
||||||
|
const defaultUnits = [{ value: "%", description: "Percentage" }];
|
||||||
|
this.populateDropdown(
|
||||||
|
elements.unit,
|
||||||
|
defaultUnits.map((unit) => unit.value),
|
||||||
|
node,
|
||||||
|
"unit"
|
||||||
|
);
|
||||||
|
console.warn(
|
||||||
|
`No matching unit category found for subType: ${selectedSubType}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching units:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
populateModels(
|
||||||
|
configUrls,
|
||||||
|
elements,
|
||||||
|
node,
|
||||||
|
selectedSupplier,
|
||||||
|
selectedSubType
|
||||||
|
) {
|
||||||
|
|
||||||
|
this.fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||||
|
.then((configData) => {
|
||||||
|
const assetType = configData.asset?.type?.default;
|
||||||
|
// save assetType to fetch later
|
||||||
|
node.assetType = assetType;
|
||||||
|
|
||||||
|
const supplierFolder = this.constructUrl( configUrls.local.taggcodeAPI,`${assetType}s`,selectedSupplier);
|
||||||
|
const subTypeFolder = this.constructUrl(supplierFolder, selectedSubType);
|
||||||
|
const localModelsUrl = this.constructUrl(subTypeFolder, "models.json");
|
||||||
|
const cloudModelsUrl = this.constructCloudURL(configUrls.cloud.taggcodeAPI, "/product/get_product_models.php?vendor_name=" + selectedSupplier + "&product_subtype_name=" + selectedSubType);
|
||||||
|
|
||||||
|
return this.fetchData(cloudModelsUrl, localModelsUrl).then((modelData) => {
|
||||||
|
const models = modelData.map((model) => model.name); // use this to populate the dropdown
|
||||||
|
|
||||||
|
// If a model is already selected, store its metadata immediately
|
||||||
|
if (node.model) {
|
||||||
|
node["modelMetadata"] = modelData.find((model) => model.name === node.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.populateDropdown(elements.model, models, node, "model", (selectedModel) => {
|
||||||
|
// Store only the metadata for the selected model
|
||||||
|
node["modelMetadata"] = modelData.find((model) => model.name === selectedModel);
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
console.log('hello here I am:');
|
||||||
|
console.log(node["modelMetadata"]);
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error populating models:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHtml(htmlElement, options, savedValue) {
|
||||||
|
htmlElement.innerHTML = options.length
|
||||||
|
? `<option value="">Select...</option>${options
|
||||||
|
.map((opt) => `<option value="${opt}">${opt}</option>`)
|
||||||
|
.join("")}`
|
||||||
|
: "<option value=''>No options available</option>";
|
||||||
|
|
||||||
|
if (savedValue && options.includes(savedValue)) {
|
||||||
|
htmlElement.value = savedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createMenuUtilsEndpoint(RED, nodeName, customHelpers = {}) {
|
||||||
|
RED.httpAdmin.get(`/${nodeName}/resources/menuUtils.js`, function(req, res) {
|
||||||
|
console.log(`Serving menuUtils.js for ${nodeName} node`);
|
||||||
|
res.set('Content-Type', 'application/javascript');
|
||||||
|
|
||||||
|
const browserCode = this.generateMenuUtilsCode(nodeName, customHelpers);
|
||||||
|
res.send(browserCode);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
generateMenuUtilsCode(nodeName, customHelpers = {}) {
|
||||||
|
const defaultHelpers = {
|
||||||
|
validateRequired: `function(value) {
|
||||||
|
return value && value.toString().trim() !== '';
|
||||||
|
}`,
|
||||||
|
formatDisplayValue: `function(value, unit) {
|
||||||
|
return \`\${value} \${unit || ''}\`.trim();
|
||||||
|
}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const allHelpers = { ...defaultHelpers, ...customHelpers };
|
||||||
|
|
||||||
|
const helpersCode = Object.entries(allHelpers)
|
||||||
|
.map(([name, func]) => ` ${name}: ${func}`)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
const classCode = MenuUtils.toString(); // <-- this gives full class MenuUtils {...}
|
||||||
|
|
||||||
|
return `
|
||||||
|
// Create EVOLV namespace structure
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
|
||||||
|
// Inject MenuUtils class
|
||||||
|
${classCode}
|
||||||
|
|
||||||
|
// Expose MenuUtils instance to namespace
|
||||||
|
window.EVOLV.nodes.${nodeName}.utils = {
|
||||||
|
menuUtils: new MenuUtils(),
|
||||||
|
|
||||||
|
helpers: {
|
||||||
|
${helpersCode}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optionally expose globally
|
||||||
|
window.MenuUtils = MenuUtils;
|
||||||
|
|
||||||
|
console.log('${nodeName} utilities loaded in namespace');
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MenuUtils;
|
||||||
@@ -390,6 +390,12 @@ class ValidationUtils {
|
|||||||
return fieldSchema.default;
|
return fieldSchema.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for uppercase characters and convert to lowercase if present
|
||||||
|
if (newConfigValue !== newConfigValue.toLowerCase()) {
|
||||||
|
this.logger.warn(`${name}.${key} contains uppercase characters. Converting to lowercase: ${newConfigValue} -> ${newConfigValue.toLowerCase()}`);
|
||||||
|
newConfigValue = newConfigValue.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
return newConfigValue;
|
return newConfigValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ class Measurement {
|
|||||||
const downIndex = downValues.timestamps.indexOf(upTimestamp);
|
const downIndex = downValues.timestamps.indexOf(upTimestamp);
|
||||||
|
|
||||||
if (downIndex !== -1) {
|
if (downIndex !== -1) {
|
||||||
|
|
||||||
const diff = upValues.values[i] - downValues.values[downIndex];
|
const diff = upValues.values[i] - downValues.values[downIndex];
|
||||||
diffMeasurement.setValue(diff, upTimestamp);
|
diffMeasurement.setValue(diff, upTimestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class MeasurementBuilder {
|
|||||||
this.position,
|
this.position,
|
||||||
this.windowSize
|
this.windowSize
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const MeasurementBuilder = require('./MeasurementBuilder');
|
const MeasurementBuilder = require('./MeasurementBuilder');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const convertModule = require('../convert/index');
|
||||||
|
|
||||||
class MeasurementContainer {
|
class MeasurementContainer {
|
||||||
constructor(options = {}, logger) {
|
constructor(options = {}) {
|
||||||
this.logger = logger;
|
this.emitter = new EventEmitter();
|
||||||
this.measurements = {};
|
this.measurements = {};
|
||||||
this.windowSize = options.windowSize || 10; // Default window size
|
this.windowSize = options.windowSize || 10; // Default window size
|
||||||
|
|
||||||
@@ -10,6 +12,63 @@ class MeasurementContainer {
|
|||||||
this._currentType = null;
|
this._currentType = null;
|
||||||
this._currentVariant = null;
|
this._currentVariant = null;
|
||||||
this._currentPosition = null;
|
this._currentPosition = null;
|
||||||
|
this._unit = null;
|
||||||
|
|
||||||
|
// Default units for each measurement type
|
||||||
|
this.defaultUnits = {
|
||||||
|
pressure: 'mbar',
|
||||||
|
flow: 'm3/h',
|
||||||
|
power: 'kW',
|
||||||
|
temperature: 'C',
|
||||||
|
volume: 'm3',
|
||||||
|
length: 'm',
|
||||||
|
...options.defaultUnits // Allow override
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-conversion settings
|
||||||
|
this.autoConvert = options.autoConvert !== false; // Default to true
|
||||||
|
this.preferredUnits = options.preferredUnits || {}; // Per-measurement overrides
|
||||||
|
|
||||||
|
// For chaining context
|
||||||
|
this._currentType = null;
|
||||||
|
this._currentVariant = null;
|
||||||
|
this._currentPosition = null;
|
||||||
|
this._unit = null;
|
||||||
|
|
||||||
|
// NEW: Enhanced child identification
|
||||||
|
this.childId = null;
|
||||||
|
this.childName = null;
|
||||||
|
this.parentRef = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Methods to set child context
|
||||||
|
setChildId(childId) {
|
||||||
|
this.childId = childId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setChildName(childName) {
|
||||||
|
this.childName = childName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setParentRef(parent) {
|
||||||
|
this.parentRef = parent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to set preferred units
|
||||||
|
setPreferredUnit(measurementType, unit) {
|
||||||
|
this.preferredUnits[measurementType] = unit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the target unit for a measurement type
|
||||||
|
_getTargetUnit(measurementType) {
|
||||||
|
return this.preferredUnits[measurementType] ||
|
||||||
|
this.defaultUnits[measurementType] ||
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chainable methods
|
// Chainable methods
|
||||||
@@ -29,28 +88,85 @@ class MeasurementContainer {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
position(positionName) {
|
position(positionValue) {
|
||||||
if (!this._currentVariant) {
|
if (!this._currentVariant) {
|
||||||
throw new Error('Variant must be specified before position');
|
throw new Error('Variant must be specified before position');
|
||||||
}
|
}
|
||||||
this._currentPosition = positionName;
|
|
||||||
|
// Turn string positions into numeric values
|
||||||
|
if (typeof positionValue == "string") {
|
||||||
|
positionValue = this._convertPositionStr2Num(positionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentPosition = positionValue;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core methods that complete the chain
|
// ENHANCED: Update your existing value method
|
||||||
value(val, timestamp = Date.now()) {
|
value(val, timestamp = Date.now(), sourceUnit = null) {
|
||||||
if (!this._ensureChainIsValid()) return this;
|
if (!this._ensureChainIsValid()) return this;
|
||||||
|
|
||||||
const measurement = this._getOrCreateMeasurement();
|
const measurement = this._getOrCreateMeasurement();
|
||||||
measurement.setValue(val, timestamp);
|
const targetUnit = this._getTargetUnit(this._currentType);
|
||||||
return this;
|
|
||||||
|
let convertedValue = val;
|
||||||
|
let finalUnit = sourceUnit || targetUnit;
|
||||||
|
|
||||||
|
// Auto-convert if enabled and units are specified
|
||||||
|
if (this.autoConvert && sourceUnit && targetUnit && sourceUnit !== targetUnit) {
|
||||||
|
try {
|
||||||
|
convertedValue = convertModule(val).from(sourceUnit).to(targetUnit);
|
||||||
|
finalUnit = targetUnit;
|
||||||
|
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.debug(`Auto-converted ${val} ${sourceUnit} to ${convertedValue} ${targetUnit}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.warn(`Auto-conversion failed from ${sourceUnit} to ${targetUnit}: ${error.message}`);
|
||||||
|
}
|
||||||
|
convertedValue = val;
|
||||||
|
finalUnit = sourceUnit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
measurement.setValue(convertedValue, timestamp);
|
||||||
|
|
||||||
|
if (finalUnit && !measurement.unit) {
|
||||||
|
measurement.setUnit(finalUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENHANCED: Emit event with rich context
|
||||||
|
const eventData = {
|
||||||
|
value: convertedValue,
|
||||||
|
originalValue: val,
|
||||||
|
unit: finalUnit,
|
||||||
|
sourceUnit: sourceUnit,
|
||||||
|
timestamp,
|
||||||
|
position: this._currentPosition,
|
||||||
|
variant: this._currentVariant,
|
||||||
|
type: this._currentType,
|
||||||
|
// NEW: Enhanced context
|
||||||
|
childId: this.childId,
|
||||||
|
childName: this.childName,
|
||||||
|
parentRef: this.parentRef
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit the exact event your parent expects
|
||||||
|
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
||||||
|
//console.log(`Emitted event: ${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
unit(unitName) {
|
unit(unitName) {
|
||||||
if (!this._ensureChainIsValid()) return this;
|
if (!this._ensureChainIsValid()) return this;
|
||||||
|
|
||||||
const measurement = this._getOrCreateMeasurement();
|
const measurement = this._getOrCreateMeasurement();
|
||||||
measurement.setUnit(unitName);
|
measurement.setUnit(unitName);
|
||||||
|
this._unit = unitName;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +176,52 @@ class MeasurementContainer {
|
|||||||
return this._getOrCreateMeasurement();
|
return this._getOrCreateMeasurement();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentValue() {
|
getCurrentValue(requestedUnit = null) {
|
||||||
const measurement = this.get();
|
const measurement = this.get();
|
||||||
return measurement ? measurement.getCurrentValue() : null;
|
if (!measurement) return null;
|
||||||
|
|
||||||
|
const value = measurement.getCurrentValue();
|
||||||
|
if (value === null) return null;
|
||||||
|
|
||||||
|
// Return as-is if no unit conversion requested
|
||||||
|
if (!requestedUnit) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert if needed
|
||||||
|
if (measurement.unit && requestedUnit !== measurement.unit) {
|
||||||
|
try {
|
||||||
|
return convertModule(value).from(measurement.unit).to(requestedUnit);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
return value; // Return original value if conversion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAverage() {
|
getAverage(requestedUnit = null) {
|
||||||
const measurement = this.get();
|
const measurement = this.get();
|
||||||
return measurement ? measurement.getAverage() : null;
|
if (!measurement) return null;
|
||||||
|
|
||||||
|
const avgValue = measurement.getAverage();
|
||||||
|
if (avgValue === null) return null;
|
||||||
|
|
||||||
|
if (!requestedUnit || !measurement.unit || requestedUnit === measurement.unit) {
|
||||||
|
return avgValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return convertModule(avgValue).from(measurement.unit).to(requestedUnit);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
return avgValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMin() {
|
getMin() {
|
||||||
@@ -85,47 +239,45 @@ class MeasurementContainer {
|
|||||||
return measurement ? measurement.getAllValues() : null;
|
return measurement ? measurement.getAllValues() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Difference calculations between positions
|
// Difference calculations between positions
|
||||||
difference() {
|
difference(requestedUnit = null) {
|
||||||
if (!this._currentType || !this._currentVariant) {
|
if (!this._currentType || !this._currentVariant) {
|
||||||
throw new Error('Type and variant must be specified for difference calculation');
|
throw new Error('Type and variant must be specified for difference calculation');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save position to restore chain state after operation
|
|
||||||
const savedPosition = this._currentPosition;
|
const savedPosition = this._currentPosition;
|
||||||
|
|
||||||
// Get upstream measurement
|
// Get upstream and downstream measurements
|
||||||
this._currentPosition = 'upstream';
|
const positions = this.getPositions();
|
||||||
|
|
||||||
|
this._currentPosition = Math.min(...positions);
|
||||||
const upstream = this.get();
|
const upstream = this.get();
|
||||||
|
|
||||||
// Get downstream measurement
|
this._currentPosition = Math.max(...positions);
|
||||||
this._currentPosition = 'downstream';
|
|
||||||
const downstream = this.get();
|
const downstream = this.get();
|
||||||
|
|
||||||
// Restore chain state
|
|
||||||
this._currentPosition = savedPosition;
|
this._currentPosition = savedPosition;
|
||||||
|
|
||||||
if (!upstream || !downstream || upstream.values.length === 0 || downstream.values.length === 0) {
|
if (!upstream || !downstream || upstream.values.length === 0 || downstream.values.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure units match
|
// Get target unit for conversion
|
||||||
let downstreamForCalc = downstream;
|
const targetUnit = requestedUnit || upstream.unit || downstream.unit;
|
||||||
if (upstream.unit && downstream.unit && upstream.unit !== downstream.unit) {
|
|
||||||
try {
|
// Get values in the same unit
|
||||||
downstreamForCalc = downstream.convertTo(upstream.unit);
|
const upstreamValue = this._convertValueToUnit(upstream.getCurrentValue(), upstream.unit, targetUnit);
|
||||||
} catch (error) {
|
const downstreamValue = this._convertValueToUnit(downstream.getCurrentValue(), downstream.unit, targetUnit);
|
||||||
if (this.logger) {
|
|
||||||
this.logger.error(`Unit conversion failed: ${error.message}`);
|
const upstreamAvg = this._convertValueToUnit(upstream.getAverage(), upstream.unit, targetUnit);
|
||||||
}
|
const downstreamAvg = this._convertValueToUnit(downstream.getAverage(), downstream.unit, targetUnit);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: downstreamForCalc.getCurrentValue() - upstream.getCurrentValue() ,
|
value: downstreamValue - upstreamValue,
|
||||||
avgDiff: downstreamForCalc.getAverage() - upstream.getAverage() ,
|
avgDiff: downstreamAvg - upstreamAvg,
|
||||||
unit: upstream.unit
|
unit: targetUnit
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +328,7 @@ class MeasurementContainer {
|
|||||||
Object.keys(this.measurements[this._currentType]) : [];
|
Object.keys(this.measurements[this._currentType]) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getPositions() {
|
getPositions(asNumber = false) {
|
||||||
if (!this._currentType || !this._currentVariant) {
|
if (!this._currentType || !this._currentVariant) {
|
||||||
throw new Error('Type and variant must be specified before listing positions');
|
throw new Error('Type and variant must be specified before listing positions');
|
||||||
}
|
}
|
||||||
@@ -185,8 +337,12 @@ class MeasurementContainer {
|
|||||||
!this.measurements[this._currentType][this._currentVariant]) {
|
!this.measurements[this._currentType][this._currentVariant]) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
|
if (asNumber) {
|
||||||
|
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(this.measurements[this._currentType][this._currentVariant]).map(this._convertPositionNum2Str);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
@@ -195,6 +351,105 @@ class MeasurementContainer {
|
|||||||
this._currentVariant = null;
|
this._currentVariant = null;
|
||||||
this._currentPosition = null;
|
this._currentPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method for value conversion
|
||||||
|
_convertValueToUnit(value, fromUnit, toUnit) {
|
||||||
|
if (!value || !fromUnit || !toUnit || fromUnit === toUnit) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return convertModule(value).from(fromUnit).to(toUnit);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.warn(`Conversion failed from ${fromUnit} to ${toUnit}: ${error.message}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available units for a measurement type
|
||||||
|
getAvailableUnits(measurementType = null) {
|
||||||
|
const type = measurementType || this._currentType;
|
||||||
|
if (!type) return [];
|
||||||
|
|
||||||
|
// Map measurement types to convert module measures
|
||||||
|
const measureMap = {
|
||||||
|
pressure: 'pressure',
|
||||||
|
flow: 'volumeFlowRate',
|
||||||
|
power: 'power',
|
||||||
|
temperature: 'temperature',
|
||||||
|
volume: 'volume',
|
||||||
|
length: 'length',
|
||||||
|
mass: 'mass',
|
||||||
|
energy: 'energy'
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertMeasure = measureMap[type];
|
||||||
|
if (!convertMeasure) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
return convertModule().possibilities(convertMeasure);
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get best unit for current value
|
||||||
|
getBestUnit(excludeUnits = []) {
|
||||||
|
const measurement = this.get();
|
||||||
|
if (!measurement || !measurement.unit) return null;
|
||||||
|
|
||||||
|
const currentValue = measurement.getCurrentValue();
|
||||||
|
if (currentValue === null) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const best = convertModule(currentValue)
|
||||||
|
.from(measurement.unit)
|
||||||
|
.toBest({ exclude: excludeUnits });
|
||||||
|
|
||||||
|
return best;
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`getBestUnit failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertPositionStr2Num(positionString) {
|
||||||
|
switch(positionString) {
|
||||||
|
case "atEquipment":
|
||||||
|
return 0;
|
||||||
|
case "upstream":
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
case "downstream":
|
||||||
|
return Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Invalid positionVsParent provided: ${positionString}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertPositionNum2Str(positionValue) {
|
||||||
|
if (positionValue === 0) {
|
||||||
|
return "atEquipment";
|
||||||
|
}
|
||||||
|
if (positionValue < 0) {
|
||||||
|
return "upstream";
|
||||||
|
}
|
||||||
|
if (positionValue > 0) {
|
||||||
|
return "downstream";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Invalid position provided: ${positionValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = MeasurementContainer;
|
module.exports = MeasurementContainer;
|
||||||
|
|||||||
@@ -1,58 +1,255 @@
|
|||||||
const { MeasurementContainer } = require('./index');
|
const { MeasurementContainer } = require('./index');
|
||||||
|
|
||||||
// Create a container
|
console.log('=== MEASUREMENT CONTAINER EXAMPLES ===\n');
|
||||||
const container = new MeasurementContainer({ windowSize: 20 });
|
console.log('This guide shows how to use the MeasurementContainer for storing,');
|
||||||
|
console.log('retrieving, and converting measurement data with automatic unit handling.\n');
|
||||||
|
|
||||||
// Example 1: Setting values with chaining
|
// ====================================
|
||||||
console.log('--- Example 1: Setting values ---');
|
// BASIC SETUP EXAMPLES
|
||||||
container.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
// ====================================
|
||||||
container.type('pressure').variant('measured').position('downstream').value(95).unit('psi');
|
console.log('--- Example 1: Basic Setup & Event Subscription ---');
|
||||||
container.type('pressure').variant('measured').position('downstream').value(80);
|
|
||||||
|
|
||||||
// Example 2: Getting values with chaining
|
// Create a basic container
|
||||||
console.log('--- Example 2: Getting values ---');
|
const basicContainer = new MeasurementContainer({ windowSize: 20 });
|
||||||
const upstreamValue = container.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
|
||||||
const upstreamUnit = container.type('pressure').variant('measured').position('upstream').get().unit;
|
// Subscribe to flow events to monitor changes
|
||||||
|
basicContainer.emitter.on('flow.predicted.upstream', (data) => {
|
||||||
|
console.log(`📡 Event: Flow predicted upstream update: ${data.value} at ${new Date(data.timestamp).toLocaleTimeString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
//show all flow values from variant measured
|
||||||
|
basicContainer.emitter.on('flow.measured.*', (data) => {
|
||||||
|
console.log(`📡 Event---------- I DID IT: Flow measured ${data.position} update: ${data.value}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Basic value setting with chaining
|
||||||
|
console.log('Setting basic pressure values...');
|
||||||
|
basicContainer.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
||||||
|
basicContainer.type('pressure').variant('measured').position('downstream').value(95).unit('psi');
|
||||||
|
basicContainer.type('pressure').variant('measured').position('downstream').value(80); // Additional value
|
||||||
|
|
||||||
|
console.log('✅ Basic setup complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// AUTO-CONVERSION SETUP EXAMPLES
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 2: Auto-Conversion Setup ---');
|
||||||
|
console.log('Setting up a container with automatic unit conversion...\n');
|
||||||
|
|
||||||
|
// Create container with auto-conversion enabled
|
||||||
|
const autoContainer = new MeasurementContainer({
|
||||||
|
autoConvert: true,
|
||||||
|
windowSize: 50,
|
||||||
|
defaultUnits: {
|
||||||
|
pressure: 'bar', // Default pressure unit
|
||||||
|
flow: 'l/min', // Default flow unit
|
||||||
|
power: 'kW', // Default power unit
|
||||||
|
temperature: 'C' // Default temperature unit
|
||||||
|
},
|
||||||
|
preferredUnits: {
|
||||||
|
pressure: 'psi' // Override: store pressure in PSI instead of bar
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Values are automatically converted to preferred units
|
||||||
|
console.log('Adding pressure data with auto-conversion:');
|
||||||
|
autoContainer.type('pressure').variant('measured').position('upstream')
|
||||||
|
.value(1.5, Date.now(), 'bar'); // Input: 1.5 bar → Auto-stored as ~21.76 psi
|
||||||
|
|
||||||
|
autoContainer.type('pressure').variant('measured').position('downstream')
|
||||||
|
.value(20, Date.now(), 'psi'); // Input: 20 psi → Stored as 20 psi (already in preferred unit)
|
||||||
|
|
||||||
|
// Check what was actually stored
|
||||||
|
const storedPressure = autoContainer.type('pressure').variant('measured').position('upstream').get();
|
||||||
|
console.log(` Stored upstream pressure: ${storedPressure.getCurrentValue()} ${storedPressure.unit}`);
|
||||||
|
console.log(' Auto-conversion setup complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// UNIT CONVERSION EXAMPLES
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 3: Unit Conversion on Retrieval ---');
|
||||||
|
console.log('Getting values in different units without changing stored data...\n');
|
||||||
|
|
||||||
|
// Add flow data in different units
|
||||||
|
autoContainer.type('flow').variant('predicted').position('upstream')
|
||||||
|
.value(100, Date.now(), 'l/min'); // Stored in l/min (default)
|
||||||
|
|
||||||
|
autoContainer.type('flow').variant('predicted').position('downstream')
|
||||||
|
.value(6, Date.now(), 'm3/h'); // Auto-converted from m3/h to l/min
|
||||||
|
|
||||||
|
// Retrieve the same data in different units
|
||||||
|
const flowLPM = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('l/min');
|
||||||
|
const flowM3H = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('m3/h');
|
||||||
|
const flowGPM = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('gal/min');
|
||||||
|
|
||||||
|
console.log(`Flow in l/min: ${flowLPM}`);
|
||||||
|
console.log(`Flow in m³/h: ${flowM3H.toFixed(2)}`);
|
||||||
|
console.log(`Flow in gal/min: ${flowGPM.toFixed(2)}`);
|
||||||
|
console.log('Unit conversion examples complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// SMART UNIT SELECTION
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 4: Smart Unit Selection ---');
|
||||||
|
console.log('Automatically finding the best unit for readability...\n');
|
||||||
|
|
||||||
|
// Add a very small pressure value
|
||||||
|
autoContainer.type('pressure').variant('test').position('sensor')
|
||||||
|
.value(0.001, Date.now(), 'bar');
|
||||||
|
|
||||||
|
// Get the best unit for this small value
|
||||||
|
const bestUnit = autoContainer.type('pressure').variant('test').position('sensor').getBestUnit();
|
||||||
|
if (bestUnit) {
|
||||||
|
console.log(`Best unit representation: ${bestUnit.val} ${bestUnit.unit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all available units for pressure
|
||||||
|
const availableUnits = autoContainer.getAvailableUnits('pressure');
|
||||||
|
console.log(`Available pressure units: ${availableUnits.slice(0, 8).join(', ')}... (${availableUnits.length} total)`);
|
||||||
|
console.log('Smart unit selection complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// BASIC RETRIEVAL AND CALCULATIONS
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 5: Basic Value Retrieval ---');
|
||||||
|
console.log('Getting individual values and their units...\n');
|
||||||
|
|
||||||
|
// Using basic container for clear examples
|
||||||
|
const upstreamValue = basicContainer.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
||||||
|
const upstreamUnit = basicContainer.type('pressure').variant('measured').position('upstream').get().unit;
|
||||||
console.log(`Upstream pressure: ${upstreamValue} ${upstreamUnit}`);
|
console.log(`Upstream pressure: ${upstreamValue} ${upstreamUnit}`);
|
||||||
const downstreamValue = container.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
|
||||||
const downstreamUnit = container.type('pressure').variant('measured').position('downstream').get().unit;
|
const downstreamValue = basicContainer.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
||||||
|
const downstreamUnit = basicContainer.type('pressure').variant('measured').position('downstream').get().unit;
|
||||||
console.log(`Downstream pressure: ${downstreamValue} ${downstreamUnit}`);
|
console.log(`Downstream pressure: ${downstreamValue} ${downstreamUnit}`);
|
||||||
|
console.log('Basic retrieval complete\n');
|
||||||
|
|
||||||
// Example 3: Calculations using chained methods
|
// ====================================
|
||||||
console.log('--- Example 3: Calculations ---');
|
// CALCULATIONS AND STATISTICS
|
||||||
container.type('flow').variant('predicted').position('upstream').value(200).unit('gpm');
|
// ====================================
|
||||||
container.type('flow').variant('predicted').position('downstream').value(195).unit('gpm');
|
console.log('--- Example 6: Calculations & Statistics ---');
|
||||||
|
console.log('Using built-in calculation methods...\n');
|
||||||
|
|
||||||
const flowAvg = container.type('flow').variant('predicted').position('upstream').getAverage();
|
// Add flow data for calculations
|
||||||
|
basicContainer.type('flow').variant('predicted').position('upstream').value(200).unit('gpm');
|
||||||
|
basicContainer.type('flow').variant('predicted').position('downstream').value(195).unit('gpm');
|
||||||
|
|
||||||
|
const flowAvg = basicContainer.type('flow').variant('predicted').position('upstream').getAverage();
|
||||||
console.log(`Average upstream flow: ${flowAvg} gpm`);
|
console.log(`Average upstream flow: ${flowAvg} gpm`);
|
||||||
|
|
||||||
// Example 4: Getting pressure difference
|
// Calculate pressure difference between upstream and downstream
|
||||||
console.log('--- Example 4: Difference calculations ---');
|
const pressureDiff = basicContainer.type('pressure').variant('measured').difference();
|
||||||
const pressureDiff = container.type('pressure').variant('measured').difference();
|
|
||||||
console.log(`Pressure difference: ${pressureDiff.value} ${pressureDiff.unit}`);
|
console.log(`Pressure difference: ${pressureDiff.value} ${pressureDiff.unit}`);
|
||||||
|
console.log('Calculations complete\n');
|
||||||
|
|
||||||
// Example 5: Adding multiple values to track history
|
// ====================================
|
||||||
console.log('--- Example 5: Multiple values ---');
|
// ADVANCED STATISTICS
|
||||||
// Add several values to upstream flow
|
// ====================================
|
||||||
container.type('flow').variant('measured').position('upstream')
|
console.log('--- Example 7: Advanced Statistics & History ---');
|
||||||
.value(210).value(215).value(205).unit('gpm');
|
console.log('Adding multiple values and getting comprehensive statistics...\n');
|
||||||
|
|
||||||
// Then get statistics
|
// Add several flow measurements to build history
|
||||||
console.log('Flow statistics:');
|
basicContainer.type('flow').variant('measured').position('upstream')
|
||||||
console.log(`- Current: ${container.type('flow').variant('measured').position('upstream').getCurrentValue()} gpm`);
|
.value(210).value(215).value(205).value(220).value(200).unit('m3/h');
|
||||||
console.log(`- Average: ${container.type('flow').variant('measured').position('upstream').getAverage()} gpm`);
|
basicContainer.type('flow').variant('measured').position('downstream')
|
||||||
console.log(`- Min: ${container.type('flow').variant('measured').position('upstream').getMin()} gpm`);
|
.value(190).value(195).value(185).value(200).value(180).unit('m3/h');
|
||||||
console.log(`- Max: ${container.type('flow').variant('measured').position('upstream').getMax()} gpm`);
|
|
||||||
console.log(`Show all values : ${JSON.stringify(container.type('flow').variant('measured').position('upstream').getAllValues())}`);
|
|
||||||
|
|
||||||
// Example 6: Listing available data
|
// Get comprehensive statistics
|
||||||
console.log('--- Example 6: Listing available data ---');
|
const measurement = basicContainer.type('flow').variant('measured').position('upstream');
|
||||||
console.log('Types:', container.getTypes());
|
console.log('Flow Statistics:');
|
||||||
console.log('Pressure variants:', container.type('pressure').getVariants());
|
console.log(`- Current value: ${measurement.getCurrentValue()} ${measurement.get().unit}`);
|
||||||
console.log('Measured pressure positions:', container.type('pressure').variant('measured').getPositions());
|
console.log(`- Average: ${measurement.getAverage().toFixed(1)} ${measurement.get().unit}`);
|
||||||
|
console.log(`- Minimum: ${measurement.getMin()} ${measurement.get().unit}`);
|
||||||
|
console.log(`- Maximum: ${measurement.getMax()} ${measurement.get().unit}`);
|
||||||
|
|
||||||
|
// Show all values with timestamps
|
||||||
|
const allValues = measurement.getAllValues();
|
||||||
|
console.log(`- Total samples: ${allValues.values.length}`);
|
||||||
|
console.log(`- Value history: [${allValues.values.join(', ')}]`);
|
||||||
|
console.log('Advanced statistics complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// DYNAMIC UNIT MANAGEMENT
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 8: Dynamic Unit Management ---');
|
||||||
|
console.log('Changing preferred units at runtime...\n');
|
||||||
|
|
||||||
|
// Change preferred unit for flow measurements
|
||||||
|
autoContainer.setPreferredUnit('flow', 'm3/h');
|
||||||
|
console.log('Changed preferred flow unit to m³/h');
|
||||||
|
|
||||||
|
// Add new flow data - will auto-convert to new preferred unit
|
||||||
|
autoContainer.type('flow').variant('realtime').position('inlet')
|
||||||
|
.value(150, Date.now(), 'l/min'); // Input in l/min, stored as m³/h
|
||||||
|
|
||||||
|
const realtimeFlow = autoContainer.type('flow').variant('realtime').position('inlet');
|
||||||
|
console.log(`Stored as: ${realtimeFlow.getCurrentValue()} ${realtimeFlow.get().unit}`);
|
||||||
|
console.log(`Original unit: ${realtimeFlow.getCurrentValue('l/min')} l/min`);
|
||||||
|
console.log('Dynamic unit management complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// DATA EXPLORATION
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 9: Data Exploration ---');
|
||||||
|
console.log('Discovering what data is available in the container...\n');
|
||||||
|
|
||||||
|
console.log('Available measurement types:', basicContainer.getTypes());
|
||||||
|
console.log('Pressure variants:', basicContainer.type('pressure').getVariants());
|
||||||
|
console.log('Measured pressure positions:', basicContainer.type('pressure').variant('measured').getPositions());
|
||||||
|
|
||||||
|
// Show data structure overview
|
||||||
|
console.log('\nData Structure Overview:');
|
||||||
|
basicContainer.getTypes().forEach(type => {
|
||||||
|
console.log(`${type.toUpperCase()}:`);
|
||||||
|
const variants = basicContainer.type(type).getVariants();
|
||||||
|
variants.forEach(variant => {
|
||||||
|
const positions = basicContainer.type(type).variant(variant).getPositions();
|
||||||
|
positions.forEach(position => {
|
||||||
|
const measurement = basicContainer.type(type).variant(variant).position(position).get();
|
||||||
|
if (measurement && measurement.values.length > 0) {
|
||||||
|
console.log(` └── ${variant}.${position}: ${measurement.values.length} values (${measurement.unit || 'no unit'})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log('Data exploration complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// BEST PRACTICES SUMMARY
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Best Practices Summary ---');
|
||||||
|
console.log('BEST PRACTICES FOR NEW USERS:\n');
|
||||||
|
|
||||||
|
console.log('1. SETUP:');
|
||||||
|
console.log(' • Enable auto-conversion for consistent units');
|
||||||
|
console.log(' • Define default units for your measurement types');
|
||||||
|
console.log(' • Set appropriate window size for your data needs\n');
|
||||||
|
|
||||||
|
console.log('2. STORING DATA:');
|
||||||
|
console.log(' • Always use the full chain: type().variant().position().value()');
|
||||||
|
console.log(' • Specify source unit when adding values: .value(100, timestamp, "psi")');
|
||||||
|
console.log(' • Set units immediately after first value: .value(100).unit("psi")\n');
|
||||||
|
|
||||||
|
console.log('3. RETRIEVING DATA:');
|
||||||
|
console.log(' • Use .getCurrentValue("unit") to get values in specific units');
|
||||||
|
console.log(' • Use .getBestUnit() for automatic unit selection');
|
||||||
|
console.log(' • Use .difference() for automatic upstream/downstream calculations\n');
|
||||||
|
|
||||||
|
console.log('4. MONITORING:');
|
||||||
|
console.log(' • Subscribe to events for real-time updates');
|
||||||
|
console.log(' • Use .emitter.on("type.variant.position", callback)');
|
||||||
|
console.log(' • Explore available data with .getTypes(), .getVariants(), .getPositions()\n');
|
||||||
|
|
||||||
|
console.log('All examples complete! Ready to use MeasurementContainer');
|
||||||
|
|
||||||
|
// Export for programmatic use
|
||||||
module.exports = {
|
module.exports = {
|
||||||
runExamples: () => {
|
runExamples: () => {
|
||||||
console.log('Examples of the measurement chainable API');
|
console.log('Measurement Container Examples - Complete Guide for New Users');
|
||||||
}
|
console.log('This file demonstrates all features with practical examples.');
|
||||||
};
|
},
|
||||||
|
|
||||||
|
// Export containers for testing
|
||||||
|
basicContainer,
|
||||||
|
autoContainer
|
||||||
|
};
|
||||||
242
src/menu/asset.js
Normal file
242
src/menu/asset.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
// asset.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class AssetMenu {
|
||||||
|
/** Define path where to find data of assets in constructor for now */
|
||||||
|
constructor(relPath = '../../datasets/assetData') {
|
||||||
|
this.baseDir = path.resolve(__dirname, relPath);
|
||||||
|
this.assetData = this._loadJSON('assetData');
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadJSON(...segments) {
|
||||||
|
const filePath = path.resolve(this.baseDir, ...segments) + '.json';
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to load ${filePath}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADD THIS METHOD
|
||||||
|
* Compiles all menu data from the file system into a single nested object.
|
||||||
|
* This is run once on the server to pre-load everything.
|
||||||
|
* @returns {object} A comprehensive object with all menu options.
|
||||||
|
*/
|
||||||
|
getAllMenuData() {
|
||||||
|
// load the raw JSON once
|
||||||
|
const data = this._loadJSON('assetData');
|
||||||
|
const allData = {};
|
||||||
|
|
||||||
|
data.suppliers.forEach(sup => {
|
||||||
|
allData[sup.name] = {};
|
||||||
|
sup.categories.forEach(cat => {
|
||||||
|
allData[sup.name][cat.name] = {};
|
||||||
|
cat.types.forEach(type => {
|
||||||
|
// here: store the full array of model objects, not just names
|
||||||
|
allData[sup.name][cat.name][type.name] = type.models;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return allData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the static initEditor function to a string that can be served to the client
|
||||||
|
* @param {string} nodeName - The name of the node type
|
||||||
|
* @returns {string} JavaScript code as a string
|
||||||
|
*/
|
||||||
|
getClientInitCode(nodeName) {
|
||||||
|
// step 1: get the two helper strings
|
||||||
|
const htmlCode = this.getHtmlInjectionCode(nodeName);
|
||||||
|
const dataCode = this.getDataInjectionCode(nodeName);
|
||||||
|
const eventsCode = this.getEventInjectionCode(nodeName);
|
||||||
|
const saveCode = this.getSaveInjectionCode(nodeName);
|
||||||
|
|
||||||
|
|
||||||
|
return `
|
||||||
|
// --- AssetMenu for ${nodeName} ---
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu =
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu || {};
|
||||||
|
|
||||||
|
${htmlCode}
|
||||||
|
${dataCode}
|
||||||
|
${eventsCode}
|
||||||
|
${saveCode}
|
||||||
|
|
||||||
|
// wire it all up when the editor loads
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = function(node) {
|
||||||
|
// ------------------ BELOW sequence is important! -------------------------------
|
||||||
|
console.log('Initializing asset properties for ${nodeName}…');
|
||||||
|
this.injectHtml();
|
||||||
|
// load the data and wire up events
|
||||||
|
// this will populate the fields and set up the event listeners
|
||||||
|
this.wireEvents(node);
|
||||||
|
// this will load the initial data into the fields
|
||||||
|
// this is important to ensure the fields are populated correctly
|
||||||
|
this.loadData(node);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// Asset Data loader for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.loadData = function(node) {
|
||||||
|
const data = window.EVOLV.nodes.${nodeName}.menuData.asset;
|
||||||
|
const elems = {
|
||||||
|
supplier: document.getElementById('node-input-supplier'),
|
||||||
|
category: document.getElementById('node-input-category'),
|
||||||
|
type: document.getElementById('node-input-assetType'),
|
||||||
|
model: document.getElementById('node-input-model'),
|
||||||
|
unit: document.getElementById('node-input-unit')
|
||||||
|
};
|
||||||
|
function populate(el, opts, sel) {
|
||||||
|
const old = el.value;
|
||||||
|
el.innerHTML = '<option value="">Select…</option>';
|
||||||
|
(opts||[]).forEach(o=>{
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = o; opt.textContent = o;
|
||||||
|
el.appendChild(opt);
|
||||||
|
});
|
||||||
|
el.value = sel||"";
|
||||||
|
if(el.value!==old) el.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
// initial population
|
||||||
|
populate(elems.supplier, Object.keys(data), node.supplier);
|
||||||
|
};
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// Asset Event wiring for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.wireEvents = function(node) {
|
||||||
|
const data = window.EVOLV.nodes.${nodeName}.menuData.asset;
|
||||||
|
const elems = {
|
||||||
|
supplier: document.getElementById('node-input-supplier'),
|
||||||
|
category: document.getElementById('node-input-category'),
|
||||||
|
type: document.getElementById('node-input-assetType'),
|
||||||
|
model: document.getElementById('node-input-model'),
|
||||||
|
unit: document.getElementById('node-input-unit')
|
||||||
|
};
|
||||||
|
function populate(el, opts, sel) {
|
||||||
|
const old = el.value;
|
||||||
|
el.innerHTML = '<option value="">Select…</option>';
|
||||||
|
(opts||[]).forEach(o=>{
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = o; opt.textContent = o;
|
||||||
|
el.appendChild(opt);
|
||||||
|
});
|
||||||
|
el.value = sel||"";
|
||||||
|
if(el.value!==old) el.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
elems.supplier.addEventListener('change', ()=>{
|
||||||
|
populate(elems.category,
|
||||||
|
elems.supplier.value? Object.keys(data[elems.supplier.value]||{}) : [],
|
||||||
|
node.category);
|
||||||
|
});
|
||||||
|
elems.category.addEventListener('change', ()=>{
|
||||||
|
const s=elems.supplier.value, c=elems.category.value;
|
||||||
|
populate(elems.type,
|
||||||
|
(s&&c)? Object.keys(data[s][c]||{}) : [],
|
||||||
|
node.assetType);
|
||||||
|
});
|
||||||
|
elems.type.addEventListener('change', ()=>{
|
||||||
|
const s=elems.supplier.value, c=elems.category.value, t=elems.type.value;
|
||||||
|
const md = (s&&c&&t)? data[s][c][t]||[] : [];
|
||||||
|
populate(elems.model, md.map(m=>m.name), node.model);
|
||||||
|
});
|
||||||
|
elems.model.addEventListener('change', ()=>{
|
||||||
|
const s=elems.supplier.value, c=elems.category.value, t=elems.type.value, m=elems.model.value;
|
||||||
|
const md = (s&&c&&t)? data[s][c][t]||[] : [];
|
||||||
|
const entry = md.find(x=>x.name===m);
|
||||||
|
populate(elems.unit, entry? entry.units : [], node.unit);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate HTML template for asset fields
|
||||||
|
*/
|
||||||
|
getHtmlTemplate() {
|
||||||
|
return `
|
||||||
|
<!-- Asset Properties -->
|
||||||
|
<hr />
|
||||||
|
<h3>Asset selection</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
|
||||||
|
<select id="node-input-supplier" style="width:70%;"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-category"><i class="fa fa-sitemap"></i> Category</label>
|
||||||
|
<select id="node-input-category" style="width:70%;"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
|
||||||
|
<select id="node-input-assetType" style="width:70%;"></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:70%;"></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:70%;"></select>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client-side HTML injection code
|
||||||
|
*/
|
||||||
|
getHtmlInjectionCode(nodeName) {
|
||||||
|
const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
||||||
|
|
||||||
|
return `
|
||||||
|
// Asset HTML injection for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() {
|
||||||
|
const placeholder = document.getElementById('asset-fields-placeholder');
|
||||||
|
if (placeholder && !placeholder.hasChildNodes()) {
|
||||||
|
placeholder.innerHTML = \`${htmlTemplate}\`;
|
||||||
|
console.log('Asset HTML injected successfully');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JS that injects the saveEditor function
|
||||||
|
*/
|
||||||
|
getSaveInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// Asset Save injection for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.saveEditor = function(node) {
|
||||||
|
console.log('Saving asset properties for ${nodeName}…');
|
||||||
|
const fields = ['supplier','category','assetType','model','unit'];
|
||||||
|
const errors = [];
|
||||||
|
fields.forEach(f => {
|
||||||
|
const el = document.getElementById(\`node-input-\${f}\`);
|
||||||
|
node[f] = el ? el.value : '';
|
||||||
|
});
|
||||||
|
if (node.assetType && !node.unit) errors.push('Unit must be set when type is specified.');
|
||||||
|
if (!node.unit) errors.push('Unit is required.');
|
||||||
|
errors.forEach(e=>RED.notify(e,'error'));
|
||||||
|
|
||||||
|
// --- DEBUG: show exactly what was saved ---
|
||||||
|
const saved = fields.reduce((o,f) => { o[f] = node[f]; return o; }, {});
|
||||||
|
console.log('→ assetMenu.saveEditor result:', saved);
|
||||||
|
|
||||||
|
return errors.length===0;
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AssetMenu;
|
||||||
175
src/menu/index.js
Normal file
175
src/menu/index.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
const AssetMenu = require('./asset.js');
|
||||||
|
const { TagcodeApp, DynamicAssetMenu } = require('./tagcodeApp.js');
|
||||||
|
const LoggerMenu = require('./logger.js');
|
||||||
|
const PhysicalPositionMenu = require('./physicalPosition.js');
|
||||||
|
|
||||||
|
class MenuManager {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.registeredMenus = new Map();
|
||||||
|
// Register factory functions
|
||||||
|
this.registerMenu('asset', () => new AssetMenu()); // static menu to be replaced by dynamic one but later
|
||||||
|
//this.registerMenu('asset', (nodeName) => new DynamicAssetMenu(nodeName, new TagcodeApp()));
|
||||||
|
this.registerMenu('logger', () => new LoggerMenu());
|
||||||
|
this.registerMenu('position', () => new PhysicalPositionMenu());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a menu type with its handler factory function
|
||||||
|
* @param {string} menuType - The type of menu (e.g., 'asset', 'logging')
|
||||||
|
* @param {function} menuFactory - The menu factory function
|
||||||
|
*/
|
||||||
|
registerMenu(menuType, menuFactory) {
|
||||||
|
this.registeredMenus.set(menuType, menuFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a complete endpoint script with data and initialization functions
|
||||||
|
* @param {string} nodeName - The name of the node type
|
||||||
|
* @param {Array<string>} menuTypes - Array of menu types to include
|
||||||
|
* @returns {string} Complete JavaScript code to serve
|
||||||
|
*/
|
||||||
|
createEndpoint(nodeName, menuTypes) {
|
||||||
|
try {
|
||||||
|
// ✅ Create instances using factory functions with proper error handling
|
||||||
|
const instantiatedMenus = new Map();
|
||||||
|
|
||||||
|
menuTypes.forEach(menuType => {
|
||||||
|
try {
|
||||||
|
const factory = this.registeredMenus.get(menuType);
|
||||||
|
if (typeof factory === 'function') {
|
||||||
|
const instance = factory(nodeName);
|
||||||
|
instantiatedMenus.set(menuType, instance);
|
||||||
|
} else {
|
||||||
|
console.warn(`No factory function found for menu type: ${menuType}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error creating instance for ${menuType}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Collect all menu data with error handling
|
||||||
|
const menuData = {};
|
||||||
|
menuTypes.forEach(menuType => {
|
||||||
|
try {
|
||||||
|
const handler = instantiatedMenus.get(menuType);
|
||||||
|
if (handler && typeof handler.getAllMenuData === 'function') {
|
||||||
|
menuData[menuType] = handler.getAllMenuData();
|
||||||
|
} else {
|
||||||
|
// Provide default empty data if method doesn't exist
|
||||||
|
menuData[menuType] = {};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting menu data for ${menuType}:`, error);
|
||||||
|
menuData[menuType] = {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Generate HTML injection code with error handling
|
||||||
|
const htmlInjections = menuTypes.map(type => {
|
||||||
|
try {
|
||||||
|
const menu = instantiatedMenus.get(type);
|
||||||
|
if (menu && typeof menu.getHtmlInjectionCode === 'function') {
|
||||||
|
return menu.getHtmlInjectionCode(nodeName);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error generating HTML injection for ${type}:`, error);
|
||||||
|
return `// Error generating HTML injection for ${type}: ${error.message}`;
|
||||||
|
}
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
// ✅ Collect all client initialization code with error handling
|
||||||
|
const initFunctions = [];
|
||||||
|
menuTypes.forEach(menuType => {
|
||||||
|
try {
|
||||||
|
const handler = instantiatedMenus.get(menuType);
|
||||||
|
if (handler && typeof handler.getClientInitCode === 'function') {
|
||||||
|
initFunctions.push(handler.getClientInitCode(nodeName));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error generating init code for ${menuType}:`, error);
|
||||||
|
initFunctions.push(`// Error in ${menuType} initialization: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert menu data to JSON
|
||||||
|
const menuDataJSON = JSON.stringify(menuData, null, 2);
|
||||||
|
|
||||||
|
// ✅ Assemble the complete script with comprehensive error handling
|
||||||
|
return `
|
||||||
|
try {
|
||||||
|
// Create the namespace structure with safety checks
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
|
||||||
|
// Initialize menu namespaces
|
||||||
|
${menuTypes.map(type => `window.EVOLV.nodes.${nodeName}.${type}Menu = window.EVOLV.nodes.${nodeName}.${type}Menu || {};`).join('\n ')}
|
||||||
|
|
||||||
|
// Inject the pre-loaded menu data directly into the namespace
|
||||||
|
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
|
||||||
|
|
||||||
|
// HTML injections with error handling
|
||||||
|
try {
|
||||||
|
${htmlInjections}
|
||||||
|
} catch (htmlError) {
|
||||||
|
console.error('Error in HTML injections for ${nodeName}:', htmlError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize functions with error handling
|
||||||
|
try {
|
||||||
|
${initFunctions.join('\n\n ')}
|
||||||
|
} catch (initError) {
|
||||||
|
console.error('Error in initialization functions for ${nodeName}:', initError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main initialization function that calls all menu initializers
|
||||||
|
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||||
|
try {
|
||||||
|
${menuTypes.map(type => `
|
||||||
|
try {
|
||||||
|
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
|
||||||
|
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
|
||||||
|
}
|
||||||
|
} catch (${type}Error) {
|
||||||
|
console.error('Error initializing ${type} menu for ${nodeName}:', ${type}Error);
|
||||||
|
}`).join('')}
|
||||||
|
} catch (editorError) {
|
||||||
|
console.error('Error in main editor initialization for ${nodeName}:', editorError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
|
||||||
|
|
||||||
|
} catch (globalError) {
|
||||||
|
console.error('Critical error in ${nodeName} menu initialization:', globalError);
|
||||||
|
|
||||||
|
// Fallback initialization
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||||
|
console.warn('Using fallback editor initialization for ${nodeName}');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Critical error creating endpoint for ${nodeName}:`, error);
|
||||||
|
|
||||||
|
// Return minimal fallback script
|
||||||
|
return `
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||||
|
console.error('Menu system failed to initialize for ${nodeName}');
|
||||||
|
};
|
||||||
|
console.error('Menu system failed for ${nodeName}:', '${error.message}');
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MenuManager;
|
||||||
134
src/menu/logger.js
Normal file
134
src/menu/logger.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
class LoggerMenu {
|
||||||
|
constructor() {
|
||||||
|
// no external data files for logger – all static
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Server‐side: return the static menuData
|
||||||
|
getAllMenuData() {
|
||||||
|
return {
|
||||||
|
logLevels: [
|
||||||
|
{ value: 'error', label: 'Error', description: 'Only error messages' },
|
||||||
|
{ value: 'warn', label: 'Warn', description: 'Warning and error messages' },
|
||||||
|
{ value: 'info', label: 'Info', description: 'Info, warning and error messages' },
|
||||||
|
{ value: 'debug', label: 'Debug', description: 'All messages including debug' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Client‐side: inject the dropdown options
|
||||||
|
getDataInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// Logger data loader for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.loggerMenu.loadData = function(node) {
|
||||||
|
const data = window.EVOLV.nodes.${nodeName}.menuData.logger;
|
||||||
|
const sel = document.getElementById('node-input-logLevel');
|
||||||
|
if (!sel) return;
|
||||||
|
sel.innerHTML = '';
|
||||||
|
data.logLevels.forEach(l => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = l.value;
|
||||||
|
opt.textContent = l.label;
|
||||||
|
opt.title = l.description;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
sel.value = node.logLevel || 'info';
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtmlInjectionCode(nodeName) {
|
||||||
|
const tpl = `
|
||||||
|
<h3>Internal logging</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-enableLog"><i class="fa fa-bug"></i>Logging</label>
|
||||||
|
<input type="checkbox" id="node-input-enableLog"/>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="row-logLevel">
|
||||||
|
<label for="node-input-logLevel"><i class="fa fa-list"></i> Log Level</label>
|
||||||
|
<select id="node-input-logLevel" style="width:60%;"></select>
|
||||||
|
</div>
|
||||||
|
`.replace(/`/g,'\\`').replace(/\$/g,'\\$');
|
||||||
|
|
||||||
|
return `
|
||||||
|
// Logger HTML injection for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.loggerMenu.injectHtml = function() {
|
||||||
|
const ph = document.getElementById('logger-fields-placeholder');
|
||||||
|
if (ph && !ph.hasChildNodes()) {
|
||||||
|
ph.innerHTML = \`${tpl}\`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Client‐side: wire up the enable‐toggle behavior
|
||||||
|
getEventInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// Logger event wiring for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.loggerMenu.wireEvents = function(node) {
|
||||||
|
const chk = document.getElementById('node-input-enableLog');
|
||||||
|
const row = document.getElementById('row-logLevel');
|
||||||
|
if (!chk || !row) return;
|
||||||
|
const toggle = () => {
|
||||||
|
row.style.display = chk.checked ? 'block' : 'none';
|
||||||
|
};
|
||||||
|
chk.checked = node.enableLog || false;
|
||||||
|
toggle();
|
||||||
|
chk.addEventListener('change', toggle);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Client‐side: save logic
|
||||||
|
getSaveInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// Logger Save injection for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.loggerMenu.saveEditor = function(node) {
|
||||||
|
console.log('Saving logger properties for ${nodeName}…');
|
||||||
|
const chk = document.getElementById('node-input-enableLog');
|
||||||
|
const sel = document.getElementById('node-input-logLevel');
|
||||||
|
node.enableLog = chk ? chk.checked : false;
|
||||||
|
node.logLevel = sel ? sel.value : 'info';
|
||||||
|
const errors = [];
|
||||||
|
if (node.enableLog && !node.logLevel) {
|
||||||
|
errors.push('Log level must be selected when logging is enabled.');
|
||||||
|
}
|
||||||
|
errors.forEach(e => RED.notify(e,'error'));
|
||||||
|
// --- DEBUG: what was saved ---
|
||||||
|
console.log('→ loggerMenu.saveEditor result:', {
|
||||||
|
enableLog: node.enableLog,
|
||||||
|
logLevel: node.logLevel
|
||||||
|
});
|
||||||
|
return errors.length === 0;
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Compose everything into one client‐side payload
|
||||||
|
getClientInitCode(nodeName) {
|
||||||
|
const dataCode = this.getDataInjectionCode(nodeName);
|
||||||
|
const eventCode = this.getEventInjectionCode(nodeName);
|
||||||
|
const saveCode = this.getSaveInjectionCode(nodeName);
|
||||||
|
const htmlCode = this.getHtmlInjectionCode(nodeName);
|
||||||
|
|
||||||
|
return `
|
||||||
|
// --- LoggerMenu for ${nodeName} ---
|
||||||
|
window.EVOLV.nodes.${nodeName}.loggerMenu =
|
||||||
|
window.EVOLV.nodes.${nodeName}.loggerMenu || {};
|
||||||
|
|
||||||
|
${htmlCode}
|
||||||
|
${dataCode}
|
||||||
|
${eventCode}
|
||||||
|
${saveCode}
|
||||||
|
|
||||||
|
// oneditprepare calls this
|
||||||
|
window.EVOLV.nodes.${nodeName}.loggerMenu.initEditor = function(node) {
|
||||||
|
// ------------------ BELOW sequence is important! -------------------------------
|
||||||
|
this.injectHtml();
|
||||||
|
this.loadData(node);
|
||||||
|
this.wireEvents(node);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LoggerMenu;
|
||||||
242
src/menu/physicalPosition.js
Normal file
242
src/menu/physicalPosition.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
|
||||||
|
class PhysicalPositionMenu {
|
||||||
|
|
||||||
|
// 1) Server-side: provide the option groups
|
||||||
|
getAllMenuData() {
|
||||||
|
return {
|
||||||
|
positionGroups: [
|
||||||
|
{ group: 'Positional', options: [
|
||||||
|
{ value: 'upstream', label: '← Upstream', icon: '←'},
|
||||||
|
{ value: 'atEquipment', label: '⊥ in place' , icon: '⊥' },
|
||||||
|
{ value: 'downstream', label: '→ Downstream' , icon: '→' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// Distance contexts for each position
|
||||||
|
distanceContexts: {
|
||||||
|
upstream: {
|
||||||
|
description: 'Distance from parent inlet',
|
||||||
|
placeholder: 'e.g., 2.5 (meters before parent)',
|
||||||
|
helpText: 'How far upstream from the parent equipment'
|
||||||
|
},
|
||||||
|
downstream: {
|
||||||
|
description: 'Distance from parent outlet',
|
||||||
|
placeholder: 'e.g., 3.0 (meters after parent)',
|
||||||
|
helpText: 'How far downstream from the parent equipment'
|
||||||
|
},
|
||||||
|
atEquipment: {
|
||||||
|
description: 'Distance from parent start',
|
||||||
|
placeholder: 'e.g., 1.2 (meters from start)',
|
||||||
|
helpText: 'Position within the parent equipment boundaries'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) HTML template (pure markup)
|
||||||
|
getHtmlTemplate() {
|
||||||
|
return `
|
||||||
|
<hr />
|
||||||
|
<h3>Physical Position vs parent</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-positionVsParent"><i class="fa fa-map-marker"></i>Position</label>
|
||||||
|
<select id="node-input-positionVsParent" style="width:70%;">
|
||||||
|
<!-- optgroups will be injected -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Distance section -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label> </label>
|
||||||
|
<input type="checkbox" id="node-input-hasDistance" style="display:inline-block; width:auto; margin-right:5px;">
|
||||||
|
<label for="node-input-hasDistance" style="width:auto;">Specify 1D Distance</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="distance-section" class="form-row" style="display:none;">
|
||||||
|
<label for="node-input-distance"><i class="fa fa-ruler"></i>Distance</label>
|
||||||
|
<div style="display:flex; align-items:center; width:70%;">
|
||||||
|
<input type="number" id="node-input-distance" step="0.1" min="0" style="width:60%;" placeholder="0.0">
|
||||||
|
<span style="margin-left:5px; margin-right:5px;">meters</span>
|
||||||
|
</div>
|
||||||
|
<div id="distance-help" class="form-tips" style="margin-left:105px; font-size:11px; color:#666;">
|
||||||
|
Select a position to see distance context
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) HTML injector
|
||||||
|
getHtmlInjectionCode(nodeName) {
|
||||||
|
const tpl = this.getHtmlTemplate()
|
||||||
|
.replace(/`/g,'\\`').replace(/\$/g,'\\$');
|
||||||
|
return `
|
||||||
|
// PhysicalPosition HTML injection for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.injectHtml = function() {
|
||||||
|
const ph = document.getElementById('position-fields-placeholder');
|
||||||
|
if (ph && !ph.hasChildNodes()) {
|
||||||
|
ph.innerHTML = \`${tpl}\`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Data-loader injector
|
||||||
|
getDataInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// PhysicalPosition data loader for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
|
||||||
|
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
||||||
|
const sel = document.getElementById('node-input-positionVsParent');
|
||||||
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
const distanceSection = document.getElementById('distance-section');
|
||||||
|
|
||||||
|
//Load position options
|
||||||
|
if (sel) {
|
||||||
|
sel.innerHTML = '';
|
||||||
|
(data.positionGroups||[]).forEach(grp => {
|
||||||
|
const optg = document.createElement('optgroup');
|
||||||
|
optg.label = grp.group;
|
||||||
|
grp.options.forEach(o=>{
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = o.value;
|
||||||
|
opt.textContent = o.label;
|
||||||
|
opt.setAttribute('data-icon', o.icon);
|
||||||
|
optg.appendChild(opt);
|
||||||
|
});
|
||||||
|
sel.appendChild(optg);
|
||||||
|
});
|
||||||
|
sel.value = node.positionVsParent || 'atEquipment';
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load distance values
|
||||||
|
if (hasDistanceCheck) {
|
||||||
|
hasDistanceCheck.checked = node.hasDistance || false;
|
||||||
|
distanceSection.style.display = hasDistanceCheck.checked ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceInput) {
|
||||||
|
distanceInput.value = node.distance || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update distance context for current position
|
||||||
|
this.updateDistanceContext(node.positionVsParent || 'atEquipment', data.distanceContexts);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) (no special events needed, but stub for symmetry)
|
||||||
|
getEventInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// PhysicalPosition events for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
|
||||||
|
const positionSel = document.getElementById('node-input-positionVsParent');
|
||||||
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceSection = document.getElementById('distance-section');
|
||||||
|
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
||||||
|
|
||||||
|
// Toggle distance section visibility
|
||||||
|
if (hasDistanceCheck && distanceSection) {
|
||||||
|
hasDistanceCheck.addEventListener('change', function() {
|
||||||
|
distanceSection.style.display = this.checked ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Clear distance if unchecked
|
||||||
|
if (!this.checked) {
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
if (distanceInput) {
|
||||||
|
distanceInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update distance context when position changes
|
||||||
|
if (positionSel) {
|
||||||
|
positionSel.addEventListener('change', function() {
|
||||||
|
const position = this.value;
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext(position, data.distanceContexts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to update distance context
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext = function(position, contexts) {
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
const distanceHelp = document.getElementById('distance-help');
|
||||||
|
|
||||||
|
const context = contexts && contexts[position];
|
||||||
|
|
||||||
|
if (context && distanceInput && distanceHelp) {
|
||||||
|
distanceInput.placeholder = context.placeholder || '0.0';
|
||||||
|
distanceHelp.textContent = context.helpText || 'Enter distance in meters';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Save-logic injector
|
||||||
|
getSaveInjectionCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// PhysicalPosition Save injection for ${nodeName}
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
|
||||||
|
const sel = document.getElementById('node-input-positionVsParent');
|
||||||
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
|
||||||
|
// Save existing position data
|
||||||
|
node.positionVsParent = sel ? sel.value : 'atEquipment';
|
||||||
|
node.positionLabel = sel ? sel.options[sel.selectedIndex].textContent : 'At Equipment';
|
||||||
|
node.positionIcon = sel ? sel.options[sel.selectedIndex].getAttribute('data-icon') : 'fa fa-cog';
|
||||||
|
|
||||||
|
// Save distance data (NEW)
|
||||||
|
node.hasDistance = hasDistanceCheck ? hasDistanceCheck.checked : false;
|
||||||
|
|
||||||
|
if (node.hasDistance && distanceInput && distanceInput.value) {
|
||||||
|
node.distance = parseFloat(distanceInput.value) || 0;
|
||||||
|
node.distanceUnit = 'm'; // Fixed to meters for now
|
||||||
|
|
||||||
|
// Generate distance description based on position
|
||||||
|
const contexts = window.EVOLV.nodes.${nodeName}.menuData.position.distanceContexts;
|
||||||
|
const context = contexts && contexts[node.positionVsParent];
|
||||||
|
node.distanceDescription = context ? context.description : 'Distance from parent';
|
||||||
|
} else {
|
||||||
|
// Clear distance data if not specified
|
||||||
|
delete node.distance;
|
||||||
|
delete node.distanceUnit;
|
||||||
|
delete node.distanceDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) Compose everything into one client bundle
|
||||||
|
getClientInitCode(nodeName) {
|
||||||
|
const htmlCode = this.getHtmlInjectionCode(nodeName);
|
||||||
|
const dataCode = this.getDataInjectionCode(nodeName);
|
||||||
|
const eventCode = this.getEventInjectionCode(nodeName);
|
||||||
|
const saveCode = this.getSaveInjectionCode(nodeName);
|
||||||
|
|
||||||
|
return `
|
||||||
|
// --- PhysicalPositionMenu for ${nodeName} ---
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu =
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu || {};
|
||||||
|
|
||||||
|
${htmlCode}
|
||||||
|
${dataCode}
|
||||||
|
${eventCode}
|
||||||
|
${saveCode}
|
||||||
|
|
||||||
|
// hook into oneditprepare
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.initEditor = function(node) {
|
||||||
|
this.injectHtml();
|
||||||
|
this.loadData(node);
|
||||||
|
this.wireEvents(node);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PhysicalPositionMenu;
|
||||||
606
src/menu/tagcodeApp.js
Normal file
606
src/menu/tagcodeApp.js
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
/**
|
||||||
|
* taggcodeApp.js
|
||||||
|
* Dynamische AssetMenu implementatie met TagcodeApp API
|
||||||
|
* Vervangt de statische assetData met calls naar REST-endpoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class TagcodeApp {
|
||||||
|
constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData(path, params = {}) {
|
||||||
|
const url = new URL(`${this.baseURL}/${path}`);
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
});
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
const json = await response.json();
|
||||||
|
if (!json.success) throw new Error(json.error || json.message);
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset endpoints
|
||||||
|
getAllAssets() {
|
||||||
|
return this.fetchData('asset/get_all_assets.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetDetail(tag_code) {
|
||||||
|
return this.fetchData('asset/get_detail_asset.php', { tag_code });
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetHistory(asset_tag_number) {
|
||||||
|
return this.fetchData('asset/get_history_asset.php', { asset_tag_number });
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetHierarchy(asset_tag_number) {
|
||||||
|
return this.fetchData('asset/get_asset_hierarchy.php', { asset_tag_number });
|
||||||
|
}
|
||||||
|
|
||||||
|
createOrUpdateAsset(params) {
|
||||||
|
return this.fetchData('asset/create_asset.php', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product & vendor endpoints
|
||||||
|
getVendors() {
|
||||||
|
return this.fetchData('vendor/get_vendors.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubtypes(vendor_name) {
|
||||||
|
return this.fetchData('product/get_subtypesFromVendor.php', { vendor_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubtypesForCategory(vendor_name, category) {
|
||||||
|
return this.fetchData('product/get_subtypesFromVendorAndCategory.php', {
|
||||||
|
vendor_name,
|
||||||
|
category
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductModels(vendor_name, product_subtype_name) {
|
||||||
|
return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocations() {
|
||||||
|
return this.fetchData('location/get_locations.php');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamicAssetMenu {
|
||||||
|
constructor(nodeName, api = new TagcodeApp()) {
|
||||||
|
|
||||||
|
this.nodeName = nodeName;
|
||||||
|
this.api = api;
|
||||||
|
|
||||||
|
//temp translation table for nodeName to API
|
||||||
|
// Mapping van nodeName naar softwareType
|
||||||
|
this.softwareTypeMapping = {
|
||||||
|
'measurement': 'Sensor',
|
||||||
|
'rotatingMachine': 'machine',
|
||||||
|
'valve': 'valve',
|
||||||
|
'pump': 'machine',
|
||||||
|
'heatExchanger': 'machine',
|
||||||
|
// Voeg meer mappings toe als nodig
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bepaal automatisch de softwareType
|
||||||
|
this.softwareType = this.softwareTypeMapping[nodeName] || nodeName;
|
||||||
|
|
||||||
|
|
||||||
|
this.data = {
|
||||||
|
vendors: [],
|
||||||
|
subtypes: {},
|
||||||
|
models: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//Added missing getAllMenuData method
|
||||||
|
|
||||||
|
getAllMenuData() {
|
||||||
|
return {
|
||||||
|
vendors: this.data.vendors || [],
|
||||||
|
locations: this.data.locations || [],
|
||||||
|
htmlTemplate: this.getHtmlTemplate()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialiseer: haal alleen de vendor-lijst en locaties op
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
this.data.suppliers = await this.api.getVendors();
|
||||||
|
this.data.locations = await this.api.getLocations();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize DynamicAssetMenu:', error);
|
||||||
|
this.data.suppliers = [];
|
||||||
|
this.data.locations = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Complete getClientInitCode method with full TagcodeApp definition
|
||||||
|
|
||||||
|
getClientInitCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// --- DynamicAssetMenu voor ${nodeName} ---
|
||||||
|
|
||||||
|
// ✅ Define COMPLETE TagcodeApp class in browser context
|
||||||
|
window.TagcodeApp = window.TagcodeApp || class {
|
||||||
|
constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData(path, params = {}) {
|
||||||
|
const url = new URL(this.baseURL + '/' + path);
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
});
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||||
|
const json = await response.json();
|
||||||
|
if (!json.success) throw new Error(json.error || json.message);
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ALL API methods defined here
|
||||||
|
getAllAssets() {
|
||||||
|
return this.fetchData('asset/get_all_assets.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetDetail(tag_code) {
|
||||||
|
return this.fetchData('asset/get_detail_asset.php', { tag_code });
|
||||||
|
}
|
||||||
|
|
||||||
|
getVendors() {
|
||||||
|
return this.fetchData('vendor/get_vendors.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubtypes(vendor_name, category = null) {
|
||||||
|
const params = { vendor_name };
|
||||||
|
if (category) params.category = category;
|
||||||
|
return this.fetchData('product/get_subtypesFromVendor.php', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductModels(vendor_name, product_subtype_name) {
|
||||||
|
return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocations() {
|
||||||
|
return this.fetchData('location/get_locations.php');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Initialize the API instance BEFORE it's needed
|
||||||
|
window.assetAPI = window.assetAPI || new window.TagcodeApp();
|
||||||
|
|
||||||
|
// Helper populate function
|
||||||
|
function populate(el, opts, sel) {
|
||||||
|
if (!el) return;
|
||||||
|
const old = el.value;
|
||||||
|
el.innerHTML = '<option value="">Select…</option>';
|
||||||
|
(opts||[]).forEach(o=>{
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = o;
|
||||||
|
opt.textContent = o;
|
||||||
|
el.appendChild(opt);
|
||||||
|
});
|
||||||
|
el.value = sel || '';
|
||||||
|
if (el.value !== old) el.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Ensure namespace exists and initialize properly
|
||||||
|
if (!window.EVOLV.nodes.${nodeName}.assetMenu) {
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Complete initEditor function
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = async function(node) {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Starting asset menu initialization for ${nodeName}');
|
||||||
|
console.log('🎯 Automatic softwareType: ${this.softwareType}');
|
||||||
|
|
||||||
|
// ✅ Verify API is available
|
||||||
|
if (!window.assetAPI) {
|
||||||
|
console.error('❌ window.assetAPI not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Wait for DOM to be ready and inject HTML with retry
|
||||||
|
const waitForDialogAndInject = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 20;
|
||||||
|
|
||||||
|
const tryInject = () => {
|
||||||
|
attempts++;
|
||||||
|
console.log('Injection attempt ' + attempts + '/' + maxAttempts);
|
||||||
|
|
||||||
|
const injectionSuccess = this.injectHtml ? this.injectHtml() : false;
|
||||||
|
|
||||||
|
if (injectionSuccess) {
|
||||||
|
console.log('✅ HTML injection successful on attempt:', attempts);
|
||||||
|
resolve(true);
|
||||||
|
} else if (attempts < maxAttempts) {
|
||||||
|
setTimeout(tryInject, 100);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ HTML injection failed after ' + maxAttempts + ' attempts');
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(tryInject, 200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for HTML injection
|
||||||
|
const htmlReady = await waitForDialogAndInject();
|
||||||
|
|
||||||
|
if (!htmlReady) {
|
||||||
|
console.error('❌ Could not inject HTML, continuing without asset menu');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 Setting up asset menu functionality');
|
||||||
|
|
||||||
|
// ✅ Load vendor list with error handling
|
||||||
|
try {
|
||||||
|
console.log('📡 Loading vendors...');
|
||||||
|
const vendors = await window.assetAPI.getVendors();
|
||||||
|
console.log('✅ Vendors loaded:', vendors.length);
|
||||||
|
|
||||||
|
// ✅ Handle both string arrays and object arrays
|
||||||
|
const vendorNames = vendors.map(v => v.name || v);
|
||||||
|
populate(document.getElementById('node-input-supplier'), vendorNames, node.supplier);
|
||||||
|
} catch (vendorError) {
|
||||||
|
console.error('❌ Error loading vendors:', vendorError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Get form elements
|
||||||
|
const elems = {
|
||||||
|
supplier: document.getElementById('node-input-supplier'),
|
||||||
|
category: document.getElementById('node-input-category'),
|
||||||
|
type: document.getElementById('node-input-assetType'),
|
||||||
|
model: document.getElementById('node-input-model'),
|
||||||
|
unit: document.getElementById('node-input-unit')
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Set automatic category value
|
||||||
|
if (elems.category) {
|
||||||
|
elems.category.value = '${this.softwareType}';
|
||||||
|
console.log('✅ Automatic category set to:', elems.category.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Supplier change: load subtypes for automatic category
|
||||||
|
if (elems.supplier) {
|
||||||
|
elems.supplier.addEventListener('change', async () => {
|
||||||
|
const vendor = elems.supplier.value;
|
||||||
|
const category = '${this.softwareType}';
|
||||||
|
|
||||||
|
if (!vendor) {
|
||||||
|
populate(elems.type, [], '');
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('📡 Loading subtypes for vendor:', vendor, 'category:', category);
|
||||||
|
const subtypes = await window.assetAPI.getSubtypes(vendor, category);
|
||||||
|
console.log('✅ Subtypes loaded:', subtypes.length);
|
||||||
|
|
||||||
|
const subtypeNames = subtypes.map(s => s.name || s.subtype_name || s);
|
||||||
|
populate(elems.type, subtypeNames, node.assetType);
|
||||||
|
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error loading subtypes:', error);
|
||||||
|
populate(elems.type, [], '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Type change: load models for vendor + selected subtype
|
||||||
|
if (elems.type) {
|
||||||
|
elems.type.addEventListener('change', async () => {
|
||||||
|
const vendor = elems.supplier.value;
|
||||||
|
const selectedSubtype = elems.type.value;
|
||||||
|
|
||||||
|
if (!vendor || !selectedSubtype) {
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('📡 Loading models for vendor:', vendor, 'subtype:', selectedSubtype);
|
||||||
|
const models = await window.assetAPI.getProductModels(vendor, selectedSubtype);
|
||||||
|
console.log('✅ Models loaded:', models.length);
|
||||||
|
|
||||||
|
window._currentModels = models;
|
||||||
|
const modelNames = models.map(m => m.name || m.model_name || m);
|
||||||
|
populate(elems.model, modelNames, node.model);
|
||||||
|
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error loading models:', error);
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Model change: show units for selected model
|
||||||
|
if (elems.model) {
|
||||||
|
elems.model.addEventListener('change', () => {
|
||||||
|
const selectedModelName = elems.model.value;
|
||||||
|
const models = window._currentModels || [];
|
||||||
|
const selectedModel = models.find(m =>
|
||||||
|
(m.name || m.model_name) === selectedModelName
|
||||||
|
);
|
||||||
|
|
||||||
|
const units = selectedModel && selectedModel.product_model_meta ?
|
||||||
|
Object.keys(selectedModel.product_model_meta) : [];
|
||||||
|
populate(elems.unit, units, node.unit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Trigger supplier change if there's a saved value
|
||||||
|
if (node.supplier && elems.supplier) {
|
||||||
|
setTimeout(() => {
|
||||||
|
elems.supplier.dispatchEvent(new Event('change'));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Asset menu initialization complete for ${nodeName}');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error in asset menu initialization:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtmlTemplate() {
|
||||||
|
return `
|
||||||
|
<!-- Asset Properties -->
|
||||||
|
<hr />
|
||||||
|
<h3>Asset selection (${this.softwareType})</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
|
||||||
|
<select id="node-input-supplier" style="width:70%;"></select>
|
||||||
|
</div>
|
||||||
|
<!-- ✅ Toon softwareType als readonly info -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label><i class="fa fa-sitemap"></i> Category</label>
|
||||||
|
<input type="text" value="${this.softwareType}" readonly style="width:70%; background-color: #f5f5f5;" />
|
||||||
|
<input type="hidden" id="node-input-category" value="${this.softwareType}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
|
||||||
|
<select id="node-input-assetType" style="width:70%;"></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:70%;"></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:70%;"></select>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed getHtmlInjectionCode method
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Fixed getHtmlInjectionCode method with better element detection
|
||||||
|
*/
|
||||||
|
getHtmlInjectionCode(nodeName) {
|
||||||
|
const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\${/g, '\\${');
|
||||||
|
|
||||||
|
return `
|
||||||
|
// Enhanced HTML injection with multiple fallback strategies
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() {
|
||||||
|
try {
|
||||||
|
// Strategy 1: Find the dialog form container
|
||||||
|
let targetContainer = document.querySelector('#red-ui-editor-dialog .red-ui-editDialog-content');
|
||||||
|
|
||||||
|
// Strategy 2: Fallback to the main dialog form
|
||||||
|
if (!targetContainer) {
|
||||||
|
targetContainer = document.querySelector('#dialog-form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Fallback to any form in the editor dialog
|
||||||
|
if (!targetContainer) {
|
||||||
|
targetContainer = document.querySelector('#red-ui-editor-dialog form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 4: Find by Red UI classes
|
||||||
|
if (!targetContainer) {
|
||||||
|
targetContainer = document.querySelector('.red-ui-editor-dialog .editor-tray-content');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetContainer) {
|
||||||
|
// Remove any existing asset menu to prevent duplicates
|
||||||
|
const existingAssetMenu = targetContainer.querySelector('.asset-menu-section');
|
||||||
|
if (existingAssetMenu) {
|
||||||
|
existingAssetMenu.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create container div
|
||||||
|
const assetMenuDiv = document.createElement('div');
|
||||||
|
assetMenuDiv.className = 'asset-menu-section';
|
||||||
|
assetMenuDiv.innerHTML = \`${htmlTemplate}\`;
|
||||||
|
|
||||||
|
// Insert at the beginning of the form
|
||||||
|
targetContainer.insertBefore(assetMenuDiv, targetContainer.firstChild);
|
||||||
|
|
||||||
|
console.log(' Asset menu HTML injected successfully into:', targetContainer.className || targetContainer.tagName);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Could not find dialog form container. Available elements:');
|
||||||
|
console.log('Available dialogs:', document.querySelectorAll('[id*="dialog"], [class*="dialog"]'));
|
||||||
|
console.log('Available forms:', document.querySelectorAll('form'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error injecting HTML:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporteer voor gebruik in Node-RED
|
||||||
|
module.exports = { TagcodeApp, DynamicAssetMenu };
|
||||||
|
|
||||||
|
/*
|
||||||
|
// --- Test CLI ---
|
||||||
|
// Voer deze test uit met `node tagcodeApp.js` om de API-client en menu-init logica te controleren
|
||||||
|
if (require.main === module) {
|
||||||
|
(async () => {
|
||||||
|
const api = new TagcodeApp();
|
||||||
|
console.log('=== Test: getVendors() ===');
|
||||||
|
let vendors;
|
||||||
|
try {
|
||||||
|
vendors = await api.getVendors();
|
||||||
|
console.log('Vendors:', vendors);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('getVendors() error:', e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== Test: getLocations() ===');
|
||||||
|
try {
|
||||||
|
const locations = await api.getLocations();
|
||||||
|
console.log('Locations:', locations);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('getLocations() error:', e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Test verschillende nodeNames met automatische softwareType mapping
|
||||||
|
const testNodes = [
|
||||||
|
{ nodeName: 'measurement', expectedSoftwareType: 'Sensor' },
|
||||||
|
{ nodeName: 'rotatingMachine', expectedSoftwareType: 'machine' },
|
||||||
|
{ nodeName: 'valve', expectedSoftwareType: 'valve' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testNode of testNodes) {
|
||||||
|
console.log(`\n=== Test: ${testNode.nodeName} → ${testNode.expectedSoftwareType} ===`);
|
||||||
|
|
||||||
|
// Initialize DynamicAssetMenu met automatische softwareType
|
||||||
|
const menu = new DynamicAssetMenu(testNode.nodeName, api);
|
||||||
|
console.log(`✅ Automatic softwareType for ${testNode.nodeName}:`, menu.softwareType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await menu.init();
|
||||||
|
console.log('Preloaded suppliers:', menu.data.suppliers.map(v=>v.name || v));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`DynamicAssetMenu.init() error for ${testNode.nodeName}:`, e.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`=== Sequential dropdown simulation for ${testNode.nodeName} ===`);
|
||||||
|
|
||||||
|
// 1. Select supplier
|
||||||
|
const supplier = menu.data.suppliers[0];
|
||||||
|
const supplierName = supplier.name || supplier;
|
||||||
|
console.log('Selected supplier:', supplierName);
|
||||||
|
|
||||||
|
// 2. ✅ Gebruik automatische softwareType in plaats van dropdown
|
||||||
|
const automaticCategory = menu.softwareType;
|
||||||
|
console.log('Automatic category (softwareType):', automaticCategory);
|
||||||
|
|
||||||
|
// 3. ✅ Direct naar models met supplier + automatische category
|
||||||
|
let models;
|
||||||
|
try {
|
||||||
|
console.log(`📡 Loading models for supplier: "${supplierName}", category: "${automaticCategory}"`);
|
||||||
|
models = await api.getProductModels(supplierName, automaticCategory);
|
||||||
|
console.log('Fetched models:', models.map(m=>m.name || m));
|
||||||
|
|
||||||
|
if (models.length === 0) {
|
||||||
|
console.warn(`⚠️ No models found for ${supplierName} + ${automaticCategory}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`getProductModels error for ${supplierName} + ${automaticCategory}:`, e.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Extract unique types from models
|
||||||
|
const types = Array.from(new Set(models.map(m => m.product_model_type || m.type || 'Unknown')));
|
||||||
|
console.log('Available types:', types);
|
||||||
|
|
||||||
|
if (types.length === 0) {
|
||||||
|
console.warn('⚠️ No types found in models');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Choose first type
|
||||||
|
const selectedType = types[0];
|
||||||
|
console.log('Selected type:', selectedType);
|
||||||
|
|
||||||
|
// 6. Filter models by type
|
||||||
|
const filteredModels = models.filter(m =>
|
||||||
|
(m.product_model_type || m.type) === selectedType
|
||||||
|
);
|
||||||
|
console.log('Models for selected type:', filteredModels.map(m => m.name || m));
|
||||||
|
|
||||||
|
if (filteredModels.length === 0) {
|
||||||
|
console.warn('⚠️ No models found for selected type');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Choose first model and show units
|
||||||
|
const model = filteredModels[0];
|
||||||
|
console.log('Selected model:', model.name || model);
|
||||||
|
|
||||||
|
const units = model.product_model_meta ? Object.keys(model.product_model_meta) : [];
|
||||||
|
console.log('Available units:', units);
|
||||||
|
const unit = units[0] || 'N/A';
|
||||||
|
console.log('Selected unit:', unit);
|
||||||
|
|
||||||
|
console.log(`✅ Complete flow for ${testNode.nodeName}:`);
|
||||||
|
console.log(` Supplier: ${supplierName}`);
|
||||||
|
console.log(` Category: ${automaticCategory} (automatic)`);
|
||||||
|
console.log(` Type: ${selectedType}`);
|
||||||
|
console.log(` Model: ${model.name || model}`);
|
||||||
|
console.log(` Unit: ${unit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Test verschillende softwareTypes ===');
|
||||||
|
|
||||||
|
// Test of de API verschillende categories ondersteunt
|
||||||
|
const testCategories = ['Sensor', 'machine', 'valve', 'pump'];
|
||||||
|
const testSupplier = 'Vega'; // Bijvoorbeeld
|
||||||
|
|
||||||
|
for (const category of testCategories) {
|
||||||
|
try {
|
||||||
|
console.log(`\n📡 Testing category: ${category} with supplier: ${testSupplier}`);
|
||||||
|
const models = await api.getProductModels(testSupplier, category);
|
||||||
|
console.log(`✅ Found ${models.length} models for ${testSupplier} + ${category}`);
|
||||||
|
|
||||||
|
if (models.length > 0) {
|
||||||
|
const sampleModel = models[0];
|
||||||
|
console.log(` Sample model:`, sampleModel.name || sampleModel);
|
||||||
|
|
||||||
|
const types = Array.from(new Set(models.map(m => m.product_model_type || m.type)));
|
||||||
|
console.log(` Available types:`, types);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`⚠️ No models found for ${testSupplier} + ${category}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Klaar met alle tests ===');
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
*/
|
||||||
387
src/predict/interpolation.js
Normal file
387
src/predict/interpolation.js
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
/**
|
||||||
|
* @file Interpolation.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.
|
||||||
|
*
|
||||||
|
* Author:
|
||||||
|
* - Rene De Ren
|
||||||
|
* Email:
|
||||||
|
* - rene@thegoldenbasket.nl
|
||||||
|
*
|
||||||
|
/*
|
||||||
|
Interpolate using cubic Hermite splines. The breakpoints in arrays xbp and ybp are assumed to be sorted.
|
||||||
|
Evaluate the function in all points of the array xeval.
|
||||||
|
Methods:
|
||||||
|
"Linear" yuck
|
||||||
|
"FiniteDifference" classic cubic interpolation, no tension parameter
|
||||||
|
"Cardinal" cubic cardinal splines, uses tension parameter which must be between [0,1]
|
||||||
|
"FritschCarlson" monotonic - tangents are first initialized, then adjusted if they are not monotonic
|
||||||
|
"FritschButland" monotonic - faster algorithm () but somewhat higher apparent "tension"
|
||||||
|
"Steffen" monotonic - also only one pass, results usualonly requires one passly between FritschCarlson and FritschButland
|
||||||
|
Sources:
|
||||||
|
Fritsch & Carlson (1980), "Monotone Piecewise Cubic Interpolation", doi:10.1137/0717021.
|
||||||
|
Fritsch & Butland (1984), "A Method for Constructing Local Monotone Piecewise Cubic Interpolants", doi:10.1137/0905021.
|
||||||
|
Steffen (1990), "A Simple Method for Monotonic Interpolation in One Dimension", http://adsabs.harvard.edu/abs/1990A%26A...239..443S
|
||||||
|
|
||||||
|
Year : (c) 2023
|
||||||
|
Author : Rene De Ren
|
||||||
|
Contact details : zn375ix3@gmail.com
|
||||||
|
Location : The Netherlands
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Interpolation {
|
||||||
|
constructor(config = {}) {
|
||||||
|
this.input_xdata = [];
|
||||||
|
this.input_ydata = [];
|
||||||
|
this.y2 = [];
|
||||||
|
this.n = 0;
|
||||||
|
this.error = 0;
|
||||||
|
this.interpolationtype = config.type || "monotone_cubic_spline";
|
||||||
|
this.tension = config.tension || 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_spline(input_xdata, input_ydata, interpolationtype) {
|
||||||
|
if (!Array.isArray(input_xdata) || !Array.isArray(input_ydata)) {
|
||||||
|
throw new Error("Invalid input: x and y must be arrays");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input_xdata.length !== input_ydata.length) {
|
||||||
|
throw new Error("Arrays x and y must have the same length");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input_xdata.length < 2) {
|
||||||
|
throw new Error("Arrays must contain at least 2 points for interpolation");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < input_xdata.length; i++) {
|
||||||
|
if (input_xdata[i] <= input_xdata[i - 1]) {
|
||||||
|
throw new Error("X values must be strictly increasing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.input_xdata = this.array_values(input_xdata);
|
||||||
|
this.input_ydata = this.array_values(input_ydata);
|
||||||
|
this.set_type(interpolationtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
array_values(obj) {
|
||||||
|
const new_array = [];
|
||||||
|
for (let i in obj) {
|
||||||
|
if (obj.hasOwnProperty(i)) {
|
||||||
|
new_array.push(obj[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new_array;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_type(type) {
|
||||||
|
if (type == "cubic_spline") {
|
||||||
|
this.cubic_spline();
|
||||||
|
} else if (type == "monotone_cubic_spline") {
|
||||||
|
this.monotonic_cubic_spline();
|
||||||
|
} else if (type == "linear") {
|
||||||
|
} else {
|
||||||
|
this.error = 1000;
|
||||||
|
}
|
||||||
|
this.interpolationtype = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate(xpoint) {
|
||||||
|
if (!this.input_xdata || !this.input_ydata || this.input_xdata.length < 2) {
|
||||||
|
throw new Error("Spline not properly initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xpoint <= this.input_xdata[0]) return this.input_ydata[0];
|
||||||
|
if (xpoint >= this.input_xdata[this.input_xdata.length - 1]) return this.input_ydata[this.input_ydata.length - 1];
|
||||||
|
|
||||||
|
let interpolatedval = 0;
|
||||||
|
|
||||||
|
if (this.interpolationtype == "cubic_spline") {
|
||||||
|
interpolatedval = this.interpolate_cubic(xpoint);
|
||||||
|
} else if (this.interpolationtype == "monotone_cubic_spline") {
|
||||||
|
interpolatedval = this.interpolate_cubic_monotonic(xpoint);
|
||||||
|
} else if (this.interpolationtype == "linear") {
|
||||||
|
interpolatedval = this.linear(xpoint);
|
||||||
|
} else {
|
||||||
|
console.log(this.interpolationtype);
|
||||||
|
interpolatedval = "Unknown type";
|
||||||
|
}
|
||||||
|
return interpolatedval;
|
||||||
|
}
|
||||||
|
|
||||||
|
cubic_spline() {
|
||||||
|
var xdata = this.input_xdata;
|
||||||
|
var ydata = this.input_ydata;
|
||||||
|
var delta = [];
|
||||||
|
|
||||||
|
var n = ydata.length;
|
||||||
|
this.n = n;
|
||||||
|
|
||||||
|
if (n !== xdata.length) {
|
||||||
|
this.error = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.y2[0] = 0.0;
|
||||||
|
this.y2[n - 1] = 0.0;
|
||||||
|
delta[0] = 0.0;
|
||||||
|
|
||||||
|
for (let i = 1; i < n - 1; ++i) {
|
||||||
|
let d = xdata[i + 1] - xdata[i - 1];
|
||||||
|
|
||||||
|
if (d == 0) {
|
||||||
|
this.error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = (xdata[i] - xdata[i - 1]) / d;
|
||||||
|
|
||||||
|
let p = s * this.y2[i - 1] + 2.0;
|
||||||
|
|
||||||
|
this.y2[i] = (s - 1.0) / p;
|
||||||
|
|
||||||
|
delta[i] = (ydata[i + 1] - ydata[i]) / (xdata[i + 1] - xdata[i]) - (ydata[i] - ydata[i - 1]) / (xdata[i] - xdata[i - 1]);
|
||||||
|
|
||||||
|
delta[i] = (6.0 * delta[i]) / (xdata[i + 1] - xdata[i - 1]) - (s * delta[i - 1]) / p;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = n - 2; j >= 0; --j) {
|
||||||
|
this.y2[j] = this.y2[j] * this.y2[j + 1] + delta[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
linear(xpoint) {
|
||||||
|
var i_min = 0;
|
||||||
|
var i_max = 0;
|
||||||
|
var o_min = 0;
|
||||||
|
var o_max = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.input_xdata.length; i++) {
|
||||||
|
if (xpoint >= this.input_xdata[i] && xpoint < this.input_xdata[i + 1]) {
|
||||||
|
i_min = this.input_xdata[i];
|
||||||
|
i_max = this.input_xdata[i + 1];
|
||||||
|
|
||||||
|
o_min = this.input_ydata[i];
|
||||||
|
o_max = this.input_ydata[i + 1];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let o_number;
|
||||||
|
|
||||||
|
if (i_min < i_max) {
|
||||||
|
o_number = o_min + ((xpoint - i_min) * (o_max - o_min)) / (i_max - i_min);
|
||||||
|
} else {
|
||||||
|
o_number = xpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return o_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate_cubic(xpoint) {
|
||||||
|
let xdata = this.input_xdata;
|
||||||
|
let ydata = this.input_ydata;
|
||||||
|
|
||||||
|
let max = this.n - 1;
|
||||||
|
let min = 0;
|
||||||
|
|
||||||
|
while (max - min > 1) {
|
||||||
|
let k = Math.floor((max + min) / 2);
|
||||||
|
|
||||||
|
if (xdata[k] > xpoint) max = k;
|
||||||
|
else min = k;
|
||||||
|
}
|
||||||
|
|
||||||
|
let h = xdata[max] - xdata[min];
|
||||||
|
|
||||||
|
if (h == 0) {
|
||||||
|
this.error = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = (xdata[max] - xpoint) / h;
|
||||||
|
let b = (xpoint - xdata[min]) / h;
|
||||||
|
|
||||||
|
let interpolatedvalue = a * ydata[min] + b * ydata[max] + ((a * a * a - a) * this.y2[min] + (b * b * b - b) * this.y2[max]) * (h * h) / 6.0;
|
||||||
|
|
||||||
|
return interpolatedvalue;
|
||||||
|
}
|
||||||
|
|
||||||
|
monotonic_cubic_spline() {
|
||||||
|
let xdata = this.input_xdata;
|
||||||
|
let ydata = this.input_ydata;
|
||||||
|
|
||||||
|
let interpolationtype = this.interpolationtype;
|
||||||
|
let tension = this.tension;
|
||||||
|
|
||||||
|
let n = ydata.length;
|
||||||
|
this.n = n;
|
||||||
|
|
||||||
|
if (this.n !== xdata.length) {
|
||||||
|
this.error = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj = this.calc_tangents(xdata, ydata, tension);
|
||||||
|
this.y1 = obj[0];
|
||||||
|
this.delta = obj[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate_cubic_monotonic(xpoint) {
|
||||||
|
let xdata = this.input_xdata;
|
||||||
|
let ydata = this.input_ydata;
|
||||||
|
let xinterval = 0;
|
||||||
|
|
||||||
|
let y1 = this.y1;
|
||||||
|
let delta = this.delta;
|
||||||
|
let c = [];
|
||||||
|
let d = [];
|
||||||
|
let n = this.n;
|
||||||
|
|
||||||
|
for (let k = 0; k < n - 1; k++) {
|
||||||
|
xinterval = xdata[k + 1] - xdata[k];
|
||||||
|
c[k] = (3 * delta[k] - 2 * y1[k] - y1[k + 1]) / xinterval;
|
||||||
|
d[k] = (y1[k] + y1[k + 1] - 2 * delta[k]) / xinterval / xinterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
let interpolatedvalues = [];
|
||||||
|
let k = 0;
|
||||||
|
|
||||||
|
if (xpoint < xdata[0] || xpoint > xdata[n - 1]) {
|
||||||
|
}
|
||||||
|
|
||||||
|
while (k < n - 1 && xpoint > xdata[k + 1] && !(xpoint < xdata[0] || xpoint > xdata[n - 1])) {
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xdiffdown = xpoint - xdata[k];
|
||||||
|
|
||||||
|
interpolatedvalues = ydata[k] + y1[k] * xdiffdown + c[k] * xdiffdown * xdiffdown + d[k] * xdiffdown * xdiffdown * xdiffdown;
|
||||||
|
|
||||||
|
return interpolatedvalues;
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_tangents(xdata, ydata, tension) {
|
||||||
|
let method = this.interpolationtype;
|
||||||
|
let n = xdata.length;
|
||||||
|
let delta_array = [];
|
||||||
|
let delta = 0;
|
||||||
|
let y1 = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < n - 1; i++) {
|
||||||
|
delta = (ydata[i + 1] - ydata[i]) / (xdata[i + 1] - xdata[i]);
|
||||||
|
delta_array[i] = delta;
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
y1[i] = delta;
|
||||||
|
} else if (method == "cardinal") {
|
||||||
|
y1[i] = (1 - tension) * (ydata[i + 1] - ydata[i - 1]) / (xdata[i + 1] - xdata[i - 1]);
|
||||||
|
} else if (method == "fritschbutland") {
|
||||||
|
let alpha = (1 + (xdata[i + 1] - xdata[i]) / (xdata[i + 1] - xdata[i - 1])) / 3;
|
||||||
|
y1[i] = delta_array[i - 1] * delta <= 0 ? 0 : (delta_array[i - 1] * delta) / (alpha * delta + (1 - alpha) * delta_array[i - 1]);
|
||||||
|
} else if (method == "fritschcarlson") {
|
||||||
|
y1[i] = delta_array[i - 1] * delta < 0 ? 0 : (delta_array[i - 1] + delta) / 2;
|
||||||
|
} else if (method == "steffen") {
|
||||||
|
let p = ((xdata[i + 1] - xdata[i]) * delta_array[i - 1] + (xdata[i] - xdata[i - 1]) * delta) / (xdata[i + 1] - xdata[i - 1]);
|
||||||
|
y1[i] = (Math.sign(delta_array[i - 1]) + Math.sign(delta)) * Math.min(Math.abs(delta_array[i - 1]), Math.abs(delta), 0.5 * Math.abs(p));
|
||||||
|
} else {
|
||||||
|
y1[i] = (delta_array[i - 1] + delta) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
y1[n - 1] = delta_array[n - 2];
|
||||||
|
if (method != "fritschcarlson") {
|
||||||
|
return [y1, delta_array];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < n - 1; i++) {
|
||||||
|
let delta = delta_array[i];
|
||||||
|
if (delta == 0) {
|
||||||
|
y1[i] = 0;
|
||||||
|
y1[i + 1] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let alpha = y1[i] / delta;
|
||||||
|
let beta = y1[i + 1] / delta;
|
||||||
|
let tau = 3 / Math.sqrt(Math.pow(alpha, 2) + Math.pow(beta, 2));
|
||||||
|
if (tau < 1) {
|
||||||
|
y1[i] = tau * alpha * delta;
|
||||||
|
y1[i + 1] = tau * beta * delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [y1, delta_array];
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate_lin_curve_points(i_curve, o_min, o_max) {
|
||||||
|
if (!Array.isArray(i_curve)) {
|
||||||
|
throw new Error("xArray must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
let o_curve = {};
|
||||||
|
let i_min = 0;
|
||||||
|
let i_max = 0;
|
||||||
|
|
||||||
|
i_min = Math.min(...Object.values(i_curve));
|
||||||
|
i_max = Math.max(...Object.values(i_curve));
|
||||||
|
|
||||||
|
i_curve.forEach((val, index) => {
|
||||||
|
o_curve[index] = this.interpolate_lin_single_point(val, i_min, i_max, o_min, o_max);
|
||||||
|
});
|
||||||
|
|
||||||
|
o_curve = Object.values(o_curve);
|
||||||
|
|
||||||
|
return o_curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate_lin_single_point(i_number, i_min, i_max, o_min, o_max) {
|
||||||
|
if (typeof i_number !== "number" || typeof i_min !== "number" || typeof i_max !== "number" || typeof o_min !== "number" || typeof o_max !== "number") {
|
||||||
|
throw new Error("All parameters must be numbers");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i_max === i_min) {
|
||||||
|
return o_min;
|
||||||
|
}
|
||||||
|
|
||||||
|
let o_number;
|
||||||
|
//i_number = this.limit_input(i_number, i_min, i_max);
|
||||||
|
|
||||||
|
o_number = o_min + ((i_number - i_min) * (o_max - o_min)) / (i_max - i_min);
|
||||||
|
|
||||||
|
o_number = this.limit_input(o_number, o_min, o_max);
|
||||||
|
|
||||||
|
return o_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
limit_input(input, min, max) {
|
||||||
|
let output;
|
||||||
|
|
||||||
|
if (input < min) {
|
||||||
|
output = min;
|
||||||
|
} else if (input > max) {
|
||||||
|
output = max;
|
||||||
|
} else {
|
||||||
|
output = input;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Interpolation;
|
||||||
199
src/predict/predictConfig.json
Normal file
199
src/predict/predictConfig.json
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "Interpolation Configuration",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this interpolation configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit used for the interpolated values (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": "interpolation",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "interpolation",
|
||||||
|
"description": "Specifies this component as an interpolation engine."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "Interpolator",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Indicates the role of this configuration (e.g., 'Interpolator', 'DataCurve', etc.)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interpolation": {
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Flag to enable/disable interpolation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "monotone_cubic_spline",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "cubic_spline",
|
||||||
|
"description": "Standard cubic spline interpolation (natural boundary)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "monotone_cubic_spline",
|
||||||
|
"description": "Monotonic cubic spline interpolation (e.g., Fritsch-Carlson)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "linear",
|
||||||
|
"description": "Basic linear interpolation between data points."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Specifies the default interpolation method."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tension": {
|
||||||
|
"default": 0.5,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"description": "Tension parameter (0–1) for spline methods like 'cardinal'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"normalization": {
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Flag to enable/disable normalization of input data."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"normalizationType": {
|
||||||
|
"default": "minmax",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "minmax",
|
||||||
|
"description": "Min-max normalization (default)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "zscore",
|
||||||
|
"description": "Z-score normalization."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Specifies the type of normalization to apply."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"min": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Minimum value for normalization."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"default": 1000,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum value for normalization."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"curvePoints": {
|
||||||
|
"default": 10,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Number of points in the normalization curve."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Normalization parameters (e.g., 'min', 'max', 'mean', 'std')."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"curve": {
|
||||||
|
"default": {
|
||||||
|
"1": {
|
||||||
|
"x": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
30,
|
||||||
|
40,
|
||||||
|
50
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "curve",
|
||||||
|
"description": "Explicitly enumerated dimension keys (no wildcard)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
593
src/predict/predict_class.js
Normal file
593
src/predict/predict_class.js
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
/**
|
||||||
|
* @file Predict_class.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 Class for predicting values based on a multidimensional curve.
|
||||||
|
* @description Class for predicting values based on a multidimensional curve.
|
||||||
|
* @module Predict_class
|
||||||
|
* @requires EventEmitter
|
||||||
|
* @requires ConfigUtils
|
||||||
|
* @requires Interpolation
|
||||||
|
* @requires Logger
|
||||||
|
* @exports Predict
|
||||||
|
* @version 0.1.0
|
||||||
|
* @since 0.1.0
|
||||||
|
*
|
||||||
|
* Author:
|
||||||
|
* - Rene De Ren
|
||||||
|
* Email:
|
||||||
|
* - rene@thegoldenbasket.nl
|
||||||
|
* Future Improvements:
|
||||||
|
- Add more interpolation types
|
||||||
|
- **Local Derivative (Slope)**: Instantaneous rate of change (dY/dX) at the current X. Useful for determining if the curve is ascending or descending.
|
||||||
|
- **Second Derivative (Curvature)**: Curvature (d²Y/dX²) at the current X. Indicates how quickly the slope is changing (e.g., sharp or broad peaks).
|
||||||
|
- **Distance to Nearest Local Peak or Valley**: X-distance from the current X to the closest local maximum or minimum. Useful for detecting proximity to turning points.
|
||||||
|
- **Global Statistics (Mean, Median, Std Dev)**:
|
||||||
|
- Mean: Average of Y.
|
||||||
|
- Median: Middle Y value (sorted).
|
||||||
|
- Std Dev: Variability of Y. Provides insight into central tendency and spread, aiding in normalization or anomaly detection.
|
||||||
|
- **Integrated Area Under the Curve (AUC)**: Numerical integration of Y across the X-range. Useful for total sums or energy-related calculations.
|
||||||
|
- **Peak “Sharpness” or “Prominence”**: Measure of a peak's height and width relative to surrounding valleys. Important for signal processing or optimization.
|
||||||
|
- **Nearest Points Around Current X**: Data points (or interpolated values) immediately to the left and right of the current X. Useful for local interpolation or neighbor analysis.
|
||||||
|
- **Forecast / Extrapolation**: Estimated Y values outside the known X-range. Useful for exploring scenarios slightly beyond the data range (use with caution).
|
||||||
|
- **Peak Count**: Total number of local maxima in the curve. Useful for identifying all peaks and their prominence.
|
||||||
|
- **Position Relative to Mean (or Other Reference Lines)**: Distance (in percent or absolute value) of the current Y from a reference line (e.g., mean or median). Provides context relative to average or baseline levels.
|
||||||
|
- **Local Slope Trend**: Direction of the slope (up, down, or flat) at the current X. Useful for identifying trends or inflection points.
|
||||||
|
- **Local Curvature Trend**: Direction of the curvature (concave up, concave down, or flat) at the current X. Useful for identifying inflection points or turning points.
|
||||||
|
- **Local Peak-to-Valley Ratio**: Ratio of the current peak height to the nearest valley depth. Useful for identifying peak prominence or sharpness.
|
||||||
|
- ** Keep track of previous request and next request to identify slope and curvature
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const Logger = require('../helper/logger.js');
|
||||||
|
const defaultConfig = require('./predictConfig.json');
|
||||||
|
const ConfigUtils = require('../helper/configUtils');
|
||||||
|
const Interpolation = require('./interpolation');
|
||||||
|
|
||||||
|
class Predict {
|
||||||
|
constructor(config = {}) {
|
||||||
|
|
||||||
|
// Initialize dependencies
|
||||||
|
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||||
|
this.configUtils = new ConfigUtils(defaultConfig);
|
||||||
|
this.config = this.configUtils.initConfig(config);
|
||||||
|
|
||||||
|
// Init after config is set
|
||||||
|
this.logger = new Logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
|
||||||
|
this.interpolation = new Interpolation(this.config.interpolation);
|
||||||
|
|
||||||
|
// Input and state
|
||||||
|
this.inputCurve = {};
|
||||||
|
this.currentF = 0;
|
||||||
|
this.currentX = 0;
|
||||||
|
this.outputY = 0;
|
||||||
|
|
||||||
|
// Curves and Splines
|
||||||
|
this.normalizedCurve = {};
|
||||||
|
this.calculatedCurve = {};
|
||||||
|
this.fCurve = {};
|
||||||
|
this.currentFxyCurve = {};
|
||||||
|
this.normalizedSplines = {};
|
||||||
|
this.fSplines = {};
|
||||||
|
this.currentFxySplines = {};
|
||||||
|
|
||||||
|
// Stored min/max values
|
||||||
|
this.xValues = {};
|
||||||
|
this.fValues = {};
|
||||||
|
this.yValues = {};
|
||||||
|
this.currentFxyXMin = 0;
|
||||||
|
this.currentFxyXMax = 0;
|
||||||
|
this.currentFxyYMin = 0;
|
||||||
|
this.currentFxyYMax = 0;
|
||||||
|
|
||||||
|
// From config
|
||||||
|
this.normMin = this.config.normalization.parameters.min;
|
||||||
|
this.normMax = this.config.normalization.parameters.max;
|
||||||
|
this.calculationPoints = this.config.normalization.parameters.curvePoints;
|
||||||
|
this.interpolationType = this.config.interpolation.type;
|
||||||
|
|
||||||
|
// Load curve if provided
|
||||||
|
if (config.curve) {
|
||||||
|
this.inputCurveData = config.curve;
|
||||||
|
} else {
|
||||||
|
this.logger.warn("No curve data provided. Please set curve data using setCurveData method. Using default");
|
||||||
|
this.inputCurveData = this.config.curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Improved function to get a local peak in an array by starting in the middle.
|
||||||
|
// It also handles the case of a tie by preferring the left side (arbitrary choice)
|
||||||
|
// when array[start] == leftValue or array[start] == rightValue.
|
||||||
|
getLocalPeak(array) {
|
||||||
|
if (!Array.isArray(array) || array.length === 0) {
|
||||||
|
return { peak: null, peakIndex: -1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = 0;
|
||||||
|
let right = array.length - 1;
|
||||||
|
|
||||||
|
while (left <= right) {
|
||||||
|
const mid = Math.floor((left + right) / 2);
|
||||||
|
|
||||||
|
// Safely retrieve left/right neighbor values (use -Infinity if out of bounds)
|
||||||
|
const leftVal = mid - 1 >= 0 ? array[mid - 1] : -Infinity;
|
||||||
|
const rightVal = mid + 1 < array.length ? array[mid + 1] : -Infinity;
|
||||||
|
const currentVal = array[mid];
|
||||||
|
|
||||||
|
// Check if mid is a local peak
|
||||||
|
if (currentVal >= leftVal && currentVal >= rightVal) {
|
||||||
|
return { peak: currentVal, peakIndex: mid };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If left neighbor is bigger, move left
|
||||||
|
if (leftVal > currentVal) {
|
||||||
|
right = mid - 1;
|
||||||
|
}
|
||||||
|
// Otherwise, move right
|
||||||
|
else {
|
||||||
|
left = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no local peak is found
|
||||||
|
return { peak: null, peakIndex: -1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function what uses the peak in the y array to return the yPeak, x value and its procentual value
|
||||||
|
getPosXofYpeak(curve) {
|
||||||
|
|
||||||
|
//find index of y peak
|
||||||
|
const { peak , peakIndex } = this.getLocalPeak(curve.y);
|
||||||
|
|
||||||
|
// scale the x value to procentual value
|
||||||
|
const yPeak = peak;
|
||||||
|
const x = curve.x[peakIndex];
|
||||||
|
const xMin = Math.min(...curve.x);
|
||||||
|
const xMax = Math.max(...curve.x);
|
||||||
|
const xProcent = (x - xMin) / (xMax - xMin) * 100;
|
||||||
|
|
||||||
|
return { yPeak, x, xProcent };
|
||||||
|
}
|
||||||
|
|
||||||
|
calcRelativePositionToPeak(curve , outputY) {
|
||||||
|
|
||||||
|
//find y peak
|
||||||
|
const { peak } = this.getLocalPeak(curve.y);
|
||||||
|
|
||||||
|
if ( peak === null ) {
|
||||||
|
this.logger.warn("No peak found in curve");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the "peak-only" percentage:
|
||||||
|
// - Distance from peak, relative to peak itself
|
||||||
|
// - 0% => outputY == peak, 100% => outputY == 0 (if peak != 0)
|
||||||
|
let peakOnlyPercentage;
|
||||||
|
const distanceFromPeak = Math.abs(peak - outputY);
|
||||||
|
if (peak === 0) {
|
||||||
|
// If peak is 0, then the concept of "peak-only" percentage is tricky.
|
||||||
|
// If outputY is also 0 => 0%, otherwise => Infinity.
|
||||||
|
peakOnlyPercentage = distanceFromPeak === 0 ? 0 : Number.POSITIVE_INFINITY;
|
||||||
|
} else {
|
||||||
|
peakOnlyPercentage = (distanceFromPeak / peak) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the range-based percentage:
|
||||||
|
// - Range = [yMin, peak]
|
||||||
|
// - 0% => outputY == peak, 100% => outputY == yMin
|
||||||
|
const yMin = Math.min(...curve.y);
|
||||||
|
let rangeBasedPercentage = -1;
|
||||||
|
|
||||||
|
// If peak <= yMin, there is no vertical range for normalization
|
||||||
|
if (peak > yMin) {
|
||||||
|
const distanceFromPeakRange = peak - outputY; // Not absolute
|
||||||
|
const totalRange = peak - yMin;
|
||||||
|
rangeBasedPercentage = (distanceFromPeakRange / totalRange) * 100;
|
||||||
|
|
||||||
|
// Optionally clamp to [0, 100] if outputY goes out of bounds
|
||||||
|
rangeBasedPercentage = Math.max(0, Math.min(100, rangeBasedPercentage));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
peakOnlyPercentage: Math.round(peakOnlyPercentage * 100) / 100,
|
||||||
|
rangeBasedPercentage: Math.round(rangeBasedPercentage * 100) / 100
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to retrieve current curve including the interpolated active point
|
||||||
|
retrieveActiveCurve(){
|
||||||
|
|
||||||
|
// Retreive y values
|
||||||
|
const yValues = this.currentFxyCurve[this.fDimension].y;
|
||||||
|
// Retreive normalized x values
|
||||||
|
const xValues = this.denormalizeXvals( this.currentFxyCurve[this.fDimension].x );
|
||||||
|
|
||||||
|
//check what the current x value is
|
||||||
|
const currentX = this.currentX;
|
||||||
|
|
||||||
|
//check current y Output value
|
||||||
|
const outputY = this.outputY;
|
||||||
|
|
||||||
|
//find where the current x value should be in the xValues array
|
||||||
|
const index = xValues.findIndex((x) => x > currentX);
|
||||||
|
|
||||||
|
// push the yOutput value in the yValues array between the current x value
|
||||||
|
yValues.splice(index, 0, outputY);
|
||||||
|
xValues.splice(index, 0, currentX);
|
||||||
|
|
||||||
|
return { xValues, yValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
set fDimension(newF) {
|
||||||
|
|
||||||
|
if (newF < this.fValues.min || newF > this.fValues.max) {
|
||||||
|
this.logger.warn(`New f =${newF} is constrained to fit between min=${this.fValues.min} and max=${this.fValues.max}`);
|
||||||
|
newF = this.constrain(newF,this.fValues.min,this.fValues.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newF in this.calculatedCurve) {
|
||||||
|
this.currentFxyCurve[newF] = this.calculatedCurve[newF];
|
||||||
|
this.currentFxySplines = this.normalizedSplines;
|
||||||
|
} else {
|
||||||
|
this.currentFxyCurve = this.buildSingleFxyCurve(
|
||||||
|
this.fSplines,
|
||||||
|
this.calculatedCurve,
|
||||||
|
newF,
|
||||||
|
this.calculationPoints
|
||||||
|
);
|
||||||
|
this.currentFxySplines = this.buildXySplines(this.currentFxyCurve, this.interpolationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yArray = this.currentFxyCurve[newF].y;
|
||||||
|
this.currentFxyYMin = Math.min(...yArray);
|
||||||
|
this.currentFxyYMax = Math.max(...yArray);
|
||||||
|
|
||||||
|
this.calculateFxyXRange(newF);
|
||||||
|
|
||||||
|
this.currentF = newF;
|
||||||
|
this.logger.debug(`Calculating new yValue using X= ${this.currentX}`);
|
||||||
|
|
||||||
|
// Recalculate output y based on currentX
|
||||||
|
this.y(this.currentX);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get fDimension() {
|
||||||
|
return this.currentF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to predict Y value based on X value
|
||||||
|
y(x) {
|
||||||
|
|
||||||
|
// Clamp value before normalization
|
||||||
|
if (x > this.currentFxyXMax) x = this.currentFxyXMax;
|
||||||
|
if (x < this.currentFxyXMin) x = this.currentFxyXMin;
|
||||||
|
|
||||||
|
//keep track of current x value
|
||||||
|
this.currentX = x;
|
||||||
|
|
||||||
|
this.logger.debug(`Interpolating x using input=${x} , currentFxyXmin=${this.currentFxyXMin}, currentFxyXMax=${this.currentFxyXMax}, normMin=${this.normMin}, normMax=${this.normMax} `);
|
||||||
|
|
||||||
|
const normalizedX = this.interpolation.interpolate_lin_single_point(
|
||||||
|
x,
|
||||||
|
this.currentFxyXMin,
|
||||||
|
this.currentFxyXMax,
|
||||||
|
this.normMin,
|
||||||
|
this.normMax
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.debug(`Calculating new Y value using ${normalizedX}`);
|
||||||
|
|
||||||
|
this.outputY = this.currentFxySplines[this.fDimension].interpolate(normalizedX);
|
||||||
|
|
||||||
|
return this.outputY;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
set yOutput(y) {
|
||||||
|
this.outputY = y;
|
||||||
|
//by emitting this one output we dont have to use the entire class
|
||||||
|
this.emitter.emit('yOutput', this.outputY);
|
||||||
|
}
|
||||||
|
|
||||||
|
get yOutput() {
|
||||||
|
return this.outputY;
|
||||||
|
}
|
||||||
|
|
||||||
|
set inputCurveData(curve) {
|
||||||
|
try {
|
||||||
|
this.inputCurve = curve;
|
||||||
|
this.buildAllFxyCurves(curve);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Curve validation failed: ${error.message}`);
|
||||||
|
this.inputCurve = null; // Reset curve data if validation fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get inputCurveData() {
|
||||||
|
return this.inputCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurve(curve) {
|
||||||
|
|
||||||
|
this.logger.info("Updating curve data");
|
||||||
|
// update config with new curve data merged with existing config
|
||||||
|
const newConfig = {...this.config, curve: curve};
|
||||||
|
this.config = this.configUtils.updateConfig(newConfig);
|
||||||
|
|
||||||
|
const validatedCurve = this.config.curve;
|
||||||
|
this.inputCurve = validatedCurve;
|
||||||
|
|
||||||
|
this.buildAllFxyCurves(validatedCurve);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
constrain(value,min,max) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAllFxyCurves(curve) {
|
||||||
|
let globalMinY = Infinity;
|
||||||
|
let globalMaxY = -Infinity;
|
||||||
|
|
||||||
|
for (const fKey of Object.keys(curve)) {
|
||||||
|
const f = Number(fKey);
|
||||||
|
this.xValues[f] = {
|
||||||
|
min: Math.min(...curve[f].x),
|
||||||
|
max: Math.max(...curve[f].x),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fMinY = Math.min(...curve[f].y);
|
||||||
|
const fMaxY = Math.max(...curve[f].y);
|
||||||
|
|
||||||
|
if (fMinY < globalMinY) globalMinY = fMinY;
|
||||||
|
if (fMaxY > globalMaxY) globalMaxY = fMaxY;
|
||||||
|
|
||||||
|
// Normalize curves
|
||||||
|
this.normalizedCurve[f] = this.normalizeCurve(curve[f], this.normMin, this.normMax);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.normalizedSplines = this.buildXySplines(this.normalizedCurve, this.interpolationType);
|
||||||
|
|
||||||
|
// Build calculated curves (same #points across all f)
|
||||||
|
for (const f of Object.keys(this.normalizedCurve)) {
|
||||||
|
this.calculatedCurve[f] = this.buildCalculatedCurve(this.normalizedSplines, f, this.calculationPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fCurve = this.buildFCurve(this.calculatedCurve, this.calculationPoints);
|
||||||
|
this.fSplines = this.buildFSplines(this.fCurve, this.interpolationType);
|
||||||
|
|
||||||
|
const fKeys = Object.keys(curve).map(Number);
|
||||||
|
this.fValues.min = Math.min(...fKeys);
|
||||||
|
this.fValues.max = Math.max(...fKeys);
|
||||||
|
|
||||||
|
this.yValues.lowest = globalMinY;
|
||||||
|
this.yValues.highest = globalMaxY;
|
||||||
|
|
||||||
|
// Set initial fDimension to min
|
||||||
|
this.fDimension = this.fValues.min;
|
||||||
|
this.logger.debug(` !!! Initial fDimension set to ${this.fValues.min}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeVal(val, normMin, normMax) {
|
||||||
|
return this.interpolation.interpolate_lin_single_point(val, normMin, normMax, 1, this.calculationPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeCurve(curve, normMin, normMax) {
|
||||||
|
return {
|
||||||
|
x: this.interpolation.interpolate_lin_curve_points(curve.x, normMin, normMax),
|
||||||
|
y: curve.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
denormalizeXvals(xValues) {
|
||||||
|
// Retrieve the normalized x-array from the current Fxy curve
|
||||||
|
const normalizedX = xValues;
|
||||||
|
|
||||||
|
// Map each normalized x to its denormalized value
|
||||||
|
const denormalizedX = normalizedX.map(nx => {
|
||||||
|
return this.interpolation.interpolate_lin_single_point(
|
||||||
|
nx,
|
||||||
|
this.normMin,
|
||||||
|
this.normMax,
|
||||||
|
this.currentFxyXMin,
|
||||||
|
this.currentFxyXMax
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a new object with denormalized x and the original y array
|
||||||
|
return denormalizedX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// interpolate input x value to denormalized x value
|
||||||
|
denormalizeX(x) {
|
||||||
|
return this.interpolation.interpolate_lin_single_point(
|
||||||
|
x,
|
||||||
|
this.normMin,
|
||||||
|
this.normMax,
|
||||||
|
this.currentFxyXMin,
|
||||||
|
this.currentFxyXMax
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCalculatedCurve(splines, f, pointsCount) {
|
||||||
|
const cCurve = { x: [], y: [] };
|
||||||
|
for (let i = 1; i <= pointsCount; i++) {
|
||||||
|
const nx = this.interpolation.interpolate_lin_single_point(i, 1, pointsCount, this.normMin, this.normMax);
|
||||||
|
cCurve.x.push(nx);
|
||||||
|
cCurve.y.push(splines[f].interpolate(nx));
|
||||||
|
}
|
||||||
|
return cCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFCurve(curve, pointsCount) {
|
||||||
|
const fCurve = {};
|
||||||
|
for (let i = 0; i < pointsCount; i++) {
|
||||||
|
fCurve[i] = { x: [], y: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < pointsCount; i++) {
|
||||||
|
for (const [f, val] of Object.entries(curve)) {
|
||||||
|
fCurve[i].x.push(Number(f));
|
||||||
|
fCurve[i].y.push(val.y[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFSplines(fCurve, type) {
|
||||||
|
const fSplines = {};
|
||||||
|
for (const i of Object.keys(fCurve)) {
|
||||||
|
fSplines[i] = this.loadSpline(fCurve[i], type);
|
||||||
|
}
|
||||||
|
return fSplines;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSingleFxyCurve(fSplines, cCurve, f, pointsCount) {
|
||||||
|
const singleCurve = { [f]: { x: [], y: [] } };
|
||||||
|
const keys = Object.keys(cCurve);
|
||||||
|
const firstKey = keys[0];
|
||||||
|
|
||||||
|
for (let i = 0; i < pointsCount; i++) {
|
||||||
|
singleCurve[f].x.push(cCurve[firstKey].x[i]);
|
||||||
|
singleCurve[f].y.push(fSplines[i].interpolate(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildXySplines(curves, type) {
|
||||||
|
const xySplines = {};
|
||||||
|
for (const f of Object.keys(curves)) {
|
||||||
|
xySplines[f] = this.loadSpline(curves[f], type);
|
||||||
|
}
|
||||||
|
return xySplines;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSpline(curve, type) {
|
||||||
|
const splineObj = new Interpolation();
|
||||||
|
splineObj.load_spline(curve.x, curve.y, type);
|
||||||
|
return splineObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateFxyXRange(value) {
|
||||||
|
|
||||||
|
const keys = Object.keys(this.inputCurve).map(Number).sort((a, b) => a - b);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const cur = keys[i];
|
||||||
|
const next = keys[i + 1];
|
||||||
|
|
||||||
|
if (value === cur) {
|
||||||
|
this.currentFxyXMin = this.xValues[cur].min;
|
||||||
|
this.currentFxyXMax = this.xValues[cur].max;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next && value > cur && value < next) {
|
||||||
|
this.currentFxyXMin = this.interpolation.interpolate_lin_single_point(
|
||||||
|
value, cur, next, this.xValues[cur].min, this.xValues[next].min
|
||||||
|
);
|
||||||
|
this.currentFxyXMax = this.interpolation.interpolate_lin_single_point(
|
||||||
|
value, cur, next, this.xValues[cur].max, this.xValues[next].max
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutput() {
|
||||||
|
return {
|
||||||
|
x: this.currentX,
|
||||||
|
y: this.yOutput,
|
||||||
|
f: this.currentF,
|
||||||
|
yOutputPosVsPeak: {
|
||||||
|
peakOnlyPercentage: this.calcRelativePositionToPeak(this.currentFxyCurve[this.fDimension], this.outputY).peakOnlyPercentage,
|
||||||
|
rangeBasedPercentage: this.calcRelativePositionToPeak(this.currentFxyCurve[this.fDimension], this.outputY).rangeBasedPercentage
|
||||||
|
},
|
||||||
|
posXyPeak: this.getPosXofYpeak(this.currentFxyCurve[this.fDimension]),
|
||||||
|
xRange: { min: this.currentFxyXMin, max: this.currentFxyXMax },
|
||||||
|
yRange: { min: this.currentFxyYMin, max: this.currentFxyYMax },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Predict;
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Example usage
|
||||||
|
let example =
|
||||||
|
{
|
||||||
|
0:
|
||||||
|
{
|
||||||
|
x:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
y:[5, 15, 25, 35, 45, 55, 45, 35, 25, 15],
|
||||||
|
},
|
||||||
|
100:
|
||||||
|
{
|
||||||
|
x:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
y:[50, 150, 250, 350, 450, 550, 450, 350, 250, 150],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//set curve data in config
|
||||||
|
let config = {curve:example};
|
||||||
|
|
||||||
|
var predict = new Predict(config=config);
|
||||||
|
|
||||||
|
console.log(" showing curve data");
|
||||||
|
console.log(predict.inputCurveData);
|
||||||
|
|
||||||
|
console.log(" showing config data");
|
||||||
|
console.log(predict.config);
|
||||||
|
|
||||||
|
// specify dimension f if there is no dim f then specify 0 as example 2
|
||||||
|
console.log(" showing config data");
|
||||||
|
console.log(predict.config);
|
||||||
|
|
||||||
|
console.log(`lowest y value ever seen : ${predict.yValues.lowest}`);
|
||||||
|
console.log(`higehst y value ever seen : ${predict.yValues.highest}`);
|
||||||
|
|
||||||
|
predict.fDimension = 0;
|
||||||
|
|
||||||
|
console.log(`default x : ${predict.currentX}`);
|
||||||
|
console.log(`min x : ${predict.currentFxyXMin} , max x : ${predict.currentFxyXMax} for f : ${predict.fDimension}`);
|
||||||
|
console.log(`min y : ${predict.currentFxyYMin} , max y : ${predict.currentFxyYMax} for f : ${predict.fDimension}`);
|
||||||
|
console.log(`Y prediction is= ${predict.outputY} @ f : ${predict.fDimension} `);
|
||||||
|
|
||||||
|
// specify x value to predict y
|
||||||
|
const yVal = predict.y(x=0);
|
||||||
|
console.log(`For x : ${predict.currentX} is the predicted value ${yVal} @ f : ${predict.fDimension} `);
|
||||||
|
console.log(predict.retrieveActiveCurve());
|
||||||
|
const peak = predict.getLocalPeak(predict.currentFxyCurve[predict.fDimension].y);
|
||||||
|
|
||||||
|
console.log(predict.getPosXofYpeak(predict.currentFxyCurve[predict.fDimension]));
|
||||||
|
|
||||||
|
const { peakOnlyPercentage, rangeBasedPercentage } = predict.calcRelativePositionToPeak(predict.currentFxyCurve[predict.fDimension], predict.outputY);
|
||||||
|
console.log(`Peak-only percentage: ${peakOnlyPercentage}%, Range-based percentage: ${rangeBasedPercentage}%`);
|
||||||
|
|
||||||
|
|
||||||
|
//*/
|
||||||
@@ -13,6 +13,7 @@ class movementManager {
|
|||||||
|
|
||||||
this.speed = speed;
|
this.speed = speed;
|
||||||
this.maxSpeed = maxSpeed;
|
this.maxSpeed = maxSpeed;
|
||||||
|
console.log(`MovementManager: Initial speed=${this.speed}, maxSpeed=${maxSpeed}`);
|
||||||
this.interval = interval;
|
this.interval = interval;
|
||||||
this.timeleft = 0; // timeleft of current movement
|
this.timeleft = 0; // timeleft of current movement
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user