Compare commits
1 Commits
6372bdc926
...
2c5704b5c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5704b5c0 |
@@ -1,17 +1,29 @@
|
|||||||
|
<!-- Load the dynamic menu & config endpoints (asset cascade + logger fields) -->
|
||||||
|
<script src="/diffuser/menu.js"></script>
|
||||||
|
<script src="/diffuser/configData.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
RED.nodes.registerType('diffuser', {
|
RED.nodes.registerType('diffuser', {
|
||||||
category: 'wbd typical',
|
category: 'EVOLV',
|
||||||
color: '#86bbdd',
|
color: '#86bbdd',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: { value: '', required: true },
|
name: { value: '', required: true },
|
||||||
number: { value: 1, required: true },
|
number: { value: 1, required: true },
|
||||||
i_elements: { value: 1, required: true },
|
i_elements: { value: 1, required: true },
|
||||||
i_diff_density: { value: 2.4, required: true },
|
i_diff_density: { value: 15, required: true },
|
||||||
i_m_water: { value: 0, required: true },
|
i_m_water: { value: 0, required: true },
|
||||||
alfaf: { value: 0.7, required: true },
|
alfaf: { value: 0.7, required: true },
|
||||||
i_zone_volume: { value: 0, required: false },
|
i_zone_volume: { value: 0, required: false },
|
||||||
processOutputFormat: { value: 'process' },
|
processOutputFormat: { value: 'process' },
|
||||||
dbaseOutputFormat: { value: 'influxdb' },
|
dbaseOutputFormat: { value: 'influxdb' },
|
||||||
|
|
||||||
|
// Asset identifier surface. supplier / assetType / category are derived
|
||||||
|
// at runtime via assetResolver.resolveAssetMetadata(model); do NOT add
|
||||||
|
// them back here. See generalFunctions/src/registry/README.md.
|
||||||
|
assetTagNumber: { value: '' },
|
||||||
|
model: { value: '' },
|
||||||
|
unit: { value: '' },
|
||||||
|
|
||||||
enableLog: { value: false },
|
enableLog: { value: false },
|
||||||
logLevel: { value: 'error' },
|
logLevel: { value: 'error' },
|
||||||
},
|
},
|
||||||
@@ -21,7 +33,36 @@ RED.nodes.registerType('diffuser', {
|
|||||||
outputLabels: ['process', 'dbase', 'parent'],
|
outputLabels: ['process', 'dbase', 'parent'],
|
||||||
icon: 'font-awesome/fa-tint',
|
icon: 'font-awesome/fa-tint',
|
||||||
label: function() {
|
label: function() {
|
||||||
return this.name ? `${this.name}_${this.number}` : 'diffuser';
|
const stem = this.model ? this.model : (this.name || 'diffuser');
|
||||||
|
return this.name ? `${this.name}_${this.number}` : stem;
|
||||||
|
},
|
||||||
|
|
||||||
|
oneditprepare: function() {
|
||||||
|
// wait for the menu scripts to load, then hand the node off to the
|
||||||
|
// shared asset / logger init code from generalFunctions
|
||||||
|
let menuRetries = 0;
|
||||||
|
const maxMenuRetries = 100; // 5 s at 50 ms intervals
|
||||||
|
const waitForMenuData = () => {
|
||||||
|
if (window.EVOLV?.nodes?.diffuser?.initEditor) {
|
||||||
|
window.EVOLV.nodes.diffuser.initEditor(this);
|
||||||
|
} else if (++menuRetries < maxMenuRetries) {
|
||||||
|
setTimeout(waitForMenuData, 50);
|
||||||
|
} else {
|
||||||
|
console.warn('diffuser: menu scripts failed to load within 5 seconds');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
waitForMenuData();
|
||||||
|
},
|
||||||
|
|
||||||
|
oneditsave: function() {
|
||||||
|
// Asset cascade saves model + unit + assetTagNumber
|
||||||
|
if (window.EVOLV?.nodes?.diffuser?.assetMenu?.saveEditor) {
|
||||||
|
window.EVOLV.nodes.diffuser.assetMenu.saveEditor(this);
|
||||||
|
}
|
||||||
|
// Logger fields
|
||||||
|
if (window.EVOLV?.nodes?.diffuser?.loggerMenu?.saveEditor) {
|
||||||
|
window.EVOLV.nodes.diffuser.loggerMenu.saveEditor(this);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -40,8 +81,11 @@ RED.nodes.registerType('diffuser', {
|
|||||||
<input type="number" id="node-input-i_elements" min="1">
|
<input type="number" id="node-input-i_elements" min="1">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-i_diff_density"><i class="fa fa-braille"></i> Density %</label>
|
<label for="node-input-i_diff_density"><i class="fa fa-braille"></i> Bottom coverage [%]</label>
|
||||||
<input type="number" id="node-input-i_diff_density" step="0.1" min="0">
|
<input type="number" id="node-input-i_diff_density" step="0.1" min="0" max="100" placeholder="typical 10-25">
|
||||||
|
<div style="font-size:11px;color:#666;margin-left:160px;">
|
||||||
|
Fraction of tank floor occupied by diffuser membrane (%). Used as the curve-family key.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-i_m_water"><i class="fa fa-arrows-v"></i> Water Height</label>
|
<label for="node-input-i_m_water"><i class="fa fa-arrows-v"></i> Water Height</label>
|
||||||
@@ -55,6 +99,7 @@ RED.nodes.registerType('diffuser', {
|
|||||||
<label for="node-input-i_zone_volume"><i class="fa fa-cube"></i> Zone Volume</label>
|
<label for="node-input-i_zone_volume"><i class="fa fa-cube"></i> Zone Volume</label>
|
||||||
<input type="number" id="node-input-i_zone_volume" step="0.1" min="0" placeholder="m3">
|
<input type="number" id="node-input-i_zone_volume" step="0.1" min="0" placeholder="m3">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Output Formats</h3>
|
<h3>Output Formats</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
||||||
@@ -72,21 +117,14 @@ RED.nodes.registerType('diffuser', {
|
|||||||
<option value="csv">csv</option>
|
<option value="csv">csv</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
|
||||||
<label for="node-input-enableLog"><i class="fa fa-book"></i> Enable Log</label>
|
<!-- Asset fields injected here by the shared asset menu (supplier → type → model → unit) -->
|
||||||
<input type="checkbox" id="node-input-enableLog" style="width: auto;">
|
<div id="asset-fields-placeholder"></div>
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
<!-- Logger fields injected here by the shared logger menu -->
|
||||||
<label for="node-input-logLevel"><i class="fa fa-signal"></i> Log Level</label>
|
<div id="logger-fields-placeholder"></div>
|
||||||
<select id="node-input-logLevel">
|
|
||||||
<option value="debug">debug</option>
|
|
||||||
<option value="info">info</option>
|
|
||||||
<option value="warn">warn</option>
|
|
||||||
<option value="error">error</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/html" data-help-name="diffuser">
|
<script type="text/html" data-help-name="diffuser">
|
||||||
<p>Diffused aeration device model.</p>
|
<p>Diffused aeration device model. Resolves a supplier curve (SSOTR vs flow per element, DWP vs flow) at the configured bottom-coverage %, and emits the resulting oxygen-transfer rate plus a zone OTR for the parent reactor.</p>
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
22
diffuser.js
22
diffuser.js
@@ -1,9 +1,31 @@
|
|||||||
const nameOfNode = 'diffuser';
|
const nameOfNode = 'diffuser';
|
||||||
const nodeClass = require('./src/nodeClass.js');
|
const nodeClass = require('./src/nodeClass.js');
|
||||||
|
const { MenuManager, configManager } = require('generalFunctions');
|
||||||
|
|
||||||
module.exports = function(RED) {
|
module.exports = function(RED) {
|
||||||
RED.nodes.registerType(nameOfNode, function(config) {
|
RED.nodes.registerType(nameOfNode, function(config) {
|
||||||
RED.nodes.createNode(this, config);
|
RED.nodes.createNode(this, config);
|
||||||
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const menuMgr = new MenuManager();
|
||||||
|
const cfgMgr = new configManager();
|
||||||
|
|
||||||
|
RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
|
||||||
|
try {
|
||||||
|
const script = menuMgr.createEndpoint(nameOfNode, ['asset', 'logger']);
|
||||||
|
res.type('application/javascript').send(script);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(`// Error generating menu: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => {
|
||||||
|
try {
|
||||||
|
const script = cfgMgr.createEndpoint(nameOfNode);
|
||||||
|
res.type('application/javascript').send(script);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(`// Error generating configData: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,11 +14,19 @@ class nodeClass extends BaseNodeAdapter {
|
|||||||
|
|
||||||
buildDomainConfig(uiConfig) {
|
buildDomainConfig(uiConfig) {
|
||||||
const n = (v, fb) => (Number.isFinite(Number(v)) ? Number(v) : fb);
|
const n = (v, fb) => (Number.isFinite(Number(v)) ? Number(v) : fb);
|
||||||
|
const s = (v, fb) => (typeof v === 'string' && v.length ? v : fb);
|
||||||
return {
|
return {
|
||||||
|
asset: {
|
||||||
|
model: s(uiConfig.model, 'gva-elastox-r'),
|
||||||
|
assetTagNumber: s(uiConfig.assetTagNumber, ''),
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
unit: s(uiConfig.unit, 'Nm3/h'),
|
||||||
|
},
|
||||||
diffuser: {
|
diffuser: {
|
||||||
number: n(uiConfig.number, 1),
|
number: n(uiConfig.number, 1),
|
||||||
elements: n(uiConfig.i_elements, 1),
|
elements: n(uiConfig.i_elements, 1),
|
||||||
density: n(uiConfig.i_diff_density, 2.4),
|
density: n(uiConfig.i_diff_density, 15),
|
||||||
waterHeight: n(uiConfig.i_m_water, 0),
|
waterHeight: n(uiConfig.i_m_water, 0),
|
||||||
alfaFactor: n(uiConfig.alfaf, 0.7),
|
alfaFactor: n(uiConfig.alfaf, 0.7),
|
||||||
headerPressure: n(uiConfig.i_pressure, 0),
|
headerPressure: n(uiConfig.i_pressure, 0),
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { BaseDomain, statusBadge, interpolation, gravity, convert } = require('generalFunctions');
|
const { BaseDomain, statusBadge, interpolation, gravity, convert, loadCurve } = require('generalFunctions');
|
||||||
|
|
||||||
|
// Default curve used when the node's asset model field is not set. Preserves
|
||||||
|
// the historical behaviour of the hardcoded _loadSpecs() (GVA ELASTOX-R at
|
||||||
|
// density 2.4 elements/m²) — the existing test suite calibrates against
|
||||||
|
// these numbers.
|
||||||
|
const DEFAULT_DIFFUSER_MODEL = 'gva-elastox-r';
|
||||||
|
|
||||||
class Diffuser extends BaseDomain {
|
class Diffuser extends BaseDomain {
|
||||||
static name = 'diffuser';
|
static name = 'diffuser';
|
||||||
@@ -19,7 +25,7 @@ class Diffuser extends BaseDomain {
|
|||||||
this.i_water_density = _num(d.waterDensity, 997);
|
this.i_water_density = _num(d.waterDensity, 997);
|
||||||
this.i_alfa_factor = _num(d.alfaFactor, 0.7);
|
this.i_alfa_factor = _num(d.alfaFactor, 0.7);
|
||||||
this.i_n_elements = _posInt(d.elements, 1);
|
this.i_n_elements = _posInt(d.elements, 1);
|
||||||
this.i_diff_density = _num(d.density, 2.4);
|
this.i_diff_density = _num(d.density, 15);
|
||||||
this.i_m_water = _num(d.waterHeight, 0);
|
this.i_m_water = _num(d.waterHeight, 0);
|
||||||
this.i_flow = 0;
|
this.i_flow = 0;
|
||||||
this.zoneVolume = _num(d.zoneVolume, 0);
|
this.zoneVolume = _num(d.zoneVolume, 0);
|
||||||
@@ -219,11 +225,27 @@ class Diffuser extends BaseDomain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_loadSpecs() {
|
_loadSpecs() {
|
||||||
|
// Curve lookup id: prefer the asset-menu-saved field, fall back to the
|
||||||
|
// legacy GVA ELASTOX-R reference (same numbers as the previous inline
|
||||||
|
// _loadSpecs). If a configured id misses the registry, fall back too —
|
||||||
|
// a missing curve would otherwise crash the constructor in production.
|
||||||
|
const cfgModel =
|
||||||
|
this.config?.asset?.model ||
|
||||||
|
this.config?.model ||
|
||||||
|
DEFAULT_DIFFUSER_MODEL;
|
||||||
|
const raw = loadCurve(cfgModel) || loadCurve(DEFAULT_DIFFUSER_MODEL);
|
||||||
|
if (!raw || !raw.otr_curve || !raw.p_curve) {
|
||||||
|
throw new Error(
|
||||||
|
`diffuser: curve '${cfgModel}' is missing otr_curve/p_curve (registry has: ${Object.keys(raw || {}).join(',') || 'nothing'})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
supplier: 'GVA', type: 'ELASTOX-R',
|
supplier: raw._meta?.supplier || null,
|
||||||
|
type: raw._meta?.type || null,
|
||||||
|
model: raw._meta?.model || cfgModel,
|
||||||
units: { Nm3: { temp: 20, pressure: 1.01325, RH: 0 } },
|
units: { Nm3: { temp: 20, pressure: 1.01325, RH: 0 } },
|
||||||
otr_curve: { 2.4: { x: [2, 3, 4, 5, 6, 7, 8, 9, 10], y: [26, 25, 24, 23.5, 23, 22.75, 22.5, 22.25, 22] } },
|
otr_curve: raw.otr_curve,
|
||||||
p_curve: { 0: { x: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], y: [40, 42.5, 45, 47.5, 50, 51.5, 53, 54.5, 56, 57.5, 59] } },
|
p_curve: raw.p_curve,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user