Compare commits

..

39 Commits

Author SHA1 Message Date
d5d078413c Add flowNumber configuration to define effluent flow handling 2025-10-31 14:03:54 +01:00
17662ef7cb Add total suspended solids sensor to assetData 2025-10-31 13:53:35 +01:00
f653a1e98c Refactor child setup to support multiple parents consistently 2025-10-24 13:37:26 +02:00
3886277616 Fix bug in parent registration code block 2025-09-29 17:13:34 +02:00
83018fabe0 Allow for multiple parents 2025-09-29 16:06:06 +02:00
e72579e5d0 Merge branch 'p.vanderwilt-main' 2025-09-26 16:18:33 +02:00
0fb42865ff Add distance configuration to measurement settings 2025-09-26 15:51:40 +02:00
b2b811e802 Add test oxygen sensor to assets 2025-09-26 14:29:14 +02:00
bde2dcf7d8 Add oygen sensor to assets 2025-09-26 14:26:41 +02:00
76570280bc Add null check for logger before logging errors in position validation 2025-09-26 13:58:09 +02:00
d7017b5d33 Add logger checks before error logging for position validation 2025-09-26 13:51:59 +02:00
f93603c182 Merge pull request 'Add distance float position handling with backward compatibility' (#1) from p.vanderwilt/generalFunctions:main into main
Reviewed-on: #1
2025-09-26 11:41:53 +00:00
c261335df5 Fix comparison operator in _convertPositionNum2Str method 2025-09-25 13:54:12 +02:00
a41f053d5d Merge branch 'position-float' 2025-09-24 13:38:50 +02:00
8d7d98f126 Fix inversion bug 2025-09-23 14:31:09 +02:00
3f90685834 Enhance position handling by adding utility methods for conversion 2025-09-23 14:17:42 +02:00
efc97d6cd1 Fix errorMetrics.js again 2025-09-23 11:55:44 +02:00
znetsixe
d72bfd5560 updated files 2025-09-22 16:02:04 +02:00
6d30e25daa Add string handling for position 2025-09-17 14:52:25 +02:00
16e202e841 Refactor position handling in MeasurementContainer to use position values instead of names 2025-09-17 13:21:35 +02:00
znetsixe
241ed1d3cb errormetrics fix 2025-09-16 12:10:41 +02:00
3876f86530 Merge branch 'main' into fix-missing-references 2025-09-15 15:11:39 +02:00
56be0f1840 Merge remote-tracking branch 'upstream/main' 2025-09-15 15:10:34 +02:00
znetsixe
a30f2c90f4 physicalPosition 1D update 2025-09-05 16:18:42 +02:00
302e122387 Fixing the same bug in reference, again 2025-09-05 13:39:15 +02:00
znetsixe
50f99fa642 updated registration logic to be consise 2025-09-04 17:06:30 +02:00
6dcd3c3d26 Merge pull request 'implement-reactor-child' (#2) from implement-reactor-child into main
Reviewed-on: p.vanderwilt/generalFunctions#2
2025-09-03 10:18:21 +00:00
958ec2269c Print reactors state after configuration 2025-09-03 11:13:00 +02:00
0bccad05f8 Added error message to node registration 2025-08-01 12:30:12 +02:00
7191e57aea Improved reactor registration 2025-07-31 14:57:38 +02:00
aec2d3692d Fixed missing reference to position 2025-07-31 11:36:42 +02:00
71643375fc Added additional reactor handling 2025-07-24 15:09:04 +02:00
f13ee68938 merge upstream 2025-07-22 11:00:42 +00:00
475caa90db Fixed bugs in connectReactor 2025-07-21 17:32:00 +02:00
9aa38f9000 Merge pull request 'Implement Reactor parent-child' (#1) from implement-reactor-child into main
Reviewed-on: p.vanderwilt/generalFunctions#1
2025-07-21 12:29:42 +00:00
4a6273b037 Merge branch 'main' into implement-reactor-child 2025-07-21 14:15:51 +02:00
8c9301b128 Remove undefined reference to 'desc' 2025-07-21 14:14:30 +02:00
7cdfc87c83 Add state update on recieving child signal 2025-07-16 16:04:32 +02:00
839ae2f3da feat: add reactor registration and handling in ChildRegistrationUtils 2025-07-16 15:34:58 +02:00
8 changed files with 237 additions and 51 deletions

View File

@@ -59,15 +59,20 @@
] ]
}, },
{ {
"name": "Level", "name": "Quantity (oxygen)",
"models": [ "models": [
{ {
"name": "VegaLevel 10", "name": "VegaOxySense 10",
"units": ["m", "ft", "mm"] "units": ["g/m³", "mol/m³"]
}, }
]
},
{
"name": "Quantity (TSS)",
"models": [
{ {
"name": "VegaLevel 20", "name": "VegaSolidsProbe",
"units": ["m", "ft", "mm"] "units": ["g/m³"]
} }
] ]
} }

View File

@@ -43,4 +43,4 @@ module.exports = {
MenuManager, MenuManager,
childRegistrationUtils, childRegistrationUtils,
loadCurve loadCurve
}; };

View File

@@ -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."
@@ -91,6 +91,13 @@
], ],
"description": "Defines the position of the measurement relative to its parent equipment or system." "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": {

View File

@@ -412,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."
}
} }
} }

View File

@@ -8,11 +8,15 @@ class ChildRegistrationUtils {
async registerChild(child, positionVsParent) { async registerChild(child, positionVsParent) {
const { softwareType } = child.config.functionality; const { softwareType } = child.config.functionality;
const { name, id } = child.config.general; const { name, id } = child.config.general;
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`); this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
// Enhanced child setup // Enhanced child setup - multiple parents
child.parent = this.mainClass; if (Array.isArray(child.parent)) {
child.parent.push(this.mainClass);
} else {
child.parent = [this.mainClass];
}
child.positionVsParent = positionVsParent; child.positionVsParent = positionVsParent;
// Enhanced measurement container with rich context // Enhanced measurement container with rich context
@@ -33,9 +37,9 @@ class ChildRegistrationUtils {
registeredAt: Date.now() registeredAt: Date.now()
}); });
// IMPORTANT: Only call parent registration - no automatic handling // IMPORTANT: Only call parent registration - no automatic handling and if parent has this function then try to register this child
if (typeof this.mainClass.registerOnChildEvents === 'function') { if (typeof this.mainClass.registerChild === 'function') {
this.mainClass.registerOnChildEvents(); this.mainClass.registerChild(child, softwareType);
} }
this.logger.info(`✅ Child ${name} registered successfully`); this.logger.info(`✅ Child ${name} registered successfully`);

View File

@@ -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);
} }

View File

@@ -88,11 +88,18 @@ 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;
} }
@@ -148,6 +155,7 @@ class MeasurementContainer {
// Emit the exact event your parent expects // Emit the exact event your parent expects
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData); this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
//console.log(`Emitted event: ${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
return this; return this;
} }
@@ -242,10 +250,12 @@ class MeasurementContainer {
const savedPosition = this._currentPosition; const savedPosition = this._currentPosition;
// Get upstream and downstream measurements // 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();
this._currentPosition = 'downstream'; this._currentPosition = Math.max(...positions);
const downstream = this.get(); const downstream = this.get();
this._currentPosition = savedPosition; this._currentPosition = savedPosition;
@@ -318,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');
} }
@@ -327,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() {
@@ -403,6 +417,39 @@ class MeasurementContainer {
} }
} }
_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;

View File

@@ -11,7 +11,25 @@ class PhysicalPositionMenu {
{ value: 'downstream', label: '→ Downstream' , 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'
}
}
}; };
} }
@@ -26,6 +44,24 @@ class PhysicalPositionMenu {
<!-- optgroups will be injected --> <!-- optgroups will be injected -->
</select> </select>
</div> </div>
<!-- Distance section -->
<div class="form-row">
<label>&nbsp;</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 /> <hr />
`; `;
} }
@@ -52,22 +88,40 @@ class PhysicalPositionMenu {
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) { window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
const data = window.EVOLV.nodes.${nodeName}.menuData.position; const data = window.EVOLV.nodes.${nodeName}.menuData.position;
const sel = document.getElementById('node-input-positionVsParent'); const sel = document.getElementById('node-input-positionVsParent');
if (!sel) return; const hasDistanceCheck = document.getElementById('node-input-hasDistance');
sel.innerHTML = ''; const distanceInput = document.getElementById('node-input-distance');
(data.positionGroups||[]).forEach(grp => { const distanceSection = document.getElementById('distance-section');
const optg = document.createElement('optgroup');
optg.label = grp.group; //Load position options
grp.options.forEach(o=>{ if (sel) {
const opt = document.createElement('option'); sel.innerHTML = '';
opt.value = o.value; (data.positionGroups||[]).forEach(grp => {
opt.textContent = o.label; const optg = document.createElement('optgroup');
opt.setAttribute('data-icon', o.icon); optg.label = grp.group;
optg.appendChild(opt); 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.appendChild(optg); sel.value = node.positionVsParent || 'atEquipment';
}); }
// default to “atEquipment” if not set
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);
}; };
`; `;
} }
@@ -77,24 +131,86 @@ class PhysicalPositionMenu {
return ` return `
// PhysicalPosition events for ${nodeName} // PhysicalPosition events for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) { window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
// no dynamic behavior 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 // 6) Save-logic injector
getSaveInjectionCode(nodeName) { getSaveInjectionCode(nodeName) {
return ` return `
// PhysicalPosition Save injection for ${nodeName} // PhysicalPosition Save injection for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) { window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
const sel = document.getElementById('node-input-positionVsParent'); const sel = document.getElementById('node-input-positionVsParent');
node.positionVsParent = sel? sel.value : 'atEquipment'; const hasDistanceCheck = document.getElementById('node-input-hasDistance');
node.positionLabel = sel? sel.options[sel.selectedIndex].textContent : 'At Equipment'; const distanceInput = document.getElementById('node-input-distance');
node.positionIcon = sel? sel.options[sel.selectedIndex].getAttribute('data-icon') : 'fa fa-cog';
return true; // 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 // 7) Compose everything into one client bundle
getClientInitCode(nodeName) { getClientInitCode(nodeName) {