Compare commits
2 Commits
d6f8af4395
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
998b2002e9 | ||
|
|
fb8d5c03e6 |
23
CLAUDE.md
Normal file
23
CLAUDE.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# measurement — Claude Code context
|
||||||
|
|
||||||
|
Sensor signal conditioning and data quality.
|
||||||
|
Part of the [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV) wastewater-automation platform.
|
||||||
|
|
||||||
|
## S88 classification
|
||||||
|
|
||||||
|
| Level | Colour | Placement lane |
|
||||||
|
|---|---|---|
|
||||||
|
| **Control Module** | `#a9daee` | L2 |
|
||||||
|
|
||||||
|
## Flow layout rules
|
||||||
|
|
||||||
|
When wiring this node into a multi-node demo or production flow, follow the
|
||||||
|
placement rule set in the **EVOLV superproject**:
|
||||||
|
|
||||||
|
> `.claude/rules/node-red-flow-layout.md` (in the EVOLV repo root)
|
||||||
|
|
||||||
|
Key points for this node:
|
||||||
|
- Place on lane **L2** (x-position per the lane table in the rule).
|
||||||
|
- Stack same-level siblings vertically.
|
||||||
|
- Parent/children sit on adjacent lanes (children one lane left, parent one lane right).
|
||||||
|
- Wrap in a Node-RED group box coloured `#a9daee` (Control Module).
|
||||||
@@ -74,44 +74,50 @@
|
|||||||
oneditprepare: function() {
|
oneditprepare: function() {
|
||||||
const node = this;
|
const node = this;
|
||||||
|
|
||||||
// === Mode selector — TOP-LEVEL hierarchy ===
|
// === Asset / logger / position placeholders (dynamic menus) ===
|
||||||
// The Input Mode drives whether analog-pipeline fields or the
|
// Kick these off FIRST so that any error in the downstream mode
|
||||||
// digital channels editor are shown. Initialize the <select> from
|
// logic can never block the shared menus. Historical regression:
|
||||||
// the saved node value, fall back to 'analog' for legacy nodes
|
// a ReferenceError in the mode block aborted oneditprepare and
|
||||||
// that were saved before the mode field existed.
|
// stopped the asset menu from rendering at all.
|
||||||
const modeSelect = document.getElementById('node-input-mode');
|
const waitForMenuData = () => {
|
||||||
const initialMode = (node.mode === 'digital' || node.mode === 'analog') ? node.mode : 'analog';
|
if (window.EVOLV?.nodes?.measurement?.initEditor) {
|
||||||
modeSelect.value = initialMode;
|
window.EVOLV.nodes.measurement.initEditor(node);
|
||||||
|
} else {
|
||||||
|
setTimeout(waitForMenuData, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
waitForMenuData();
|
||||||
|
|
||||||
|
// IMPORTANT: all DOM references are resolved up front so helper
|
||||||
|
// functions called during initial applyMode() don't trip over the
|
||||||
|
// Temporal Dead Zone on later `const` declarations.
|
||||||
|
|
||||||
|
const modeSelect = document.getElementById('node-input-mode');
|
||||||
const analogBlock = document.getElementById('analog-only-fields');
|
const analogBlock = document.getElementById('analog-only-fields');
|
||||||
const digitalBlock = document.getElementById('digital-only-fields');
|
const digitalBlock = document.getElementById('digital-only-fields');
|
||||||
const modeHint = document.getElementById('mode-hint');
|
const modeHint = document.getElementById('mode-hint');
|
||||||
|
|
||||||
function applyMode(mode) {
|
|
||||||
const isDigital = mode === 'digital';
|
|
||||||
analogBlock.style.display = isDigital ? 'none' : 'block';
|
|
||||||
digitalBlock.style.display = isDigital ? 'block' : 'none';
|
|
||||||
if (modeHint) {
|
|
||||||
modeHint.textContent = isDigital
|
|
||||||
? 'msg.payload must be an OBJECT, e.g. {"temperature": 22.5, "humidity": 45}. Define each key below.'
|
|
||||||
: 'msg.payload must be a NUMBER (or numeric string). Configure scaling/smoothing below.';
|
|
||||||
}
|
|
||||||
validateChannelsJson();
|
|
||||||
}
|
|
||||||
|
|
||||||
modeSelect.addEventListener('change', (e) => applyMode(e.target.value));
|
|
||||||
applyMode(initialMode);
|
|
||||||
|
|
||||||
// === Channels JSON live validation (digital only) ===
|
|
||||||
const channelsArea = document.getElementById('node-input-channels');
|
const channelsArea = document.getElementById('node-input-channels');
|
||||||
const channelsHint = document.getElementById('channels-validation');
|
const channelsHint = document.getElementById('channels-validation');
|
||||||
|
|
||||||
|
// Initialise the mode <select> from the saved node.mode. Legacy
|
||||||
|
// nodes (saved before the mode field existed) fall back to
|
||||||
|
// 'analog' so they keep behaving exactly like before.
|
||||||
|
const initialMode = (node.mode === 'digital' || node.mode === 'analog') ? node.mode : 'analog';
|
||||||
|
if (modeSelect) modeSelect.value = initialMode;
|
||||||
|
|
||||||
|
// Populate the channels textarea from the saved node.channels
|
||||||
|
// (stored as a raw JSON string; parsing happens server-side).
|
||||||
if (channelsArea && typeof node.channels === 'string') {
|
if (channelsArea && typeof node.channels === 'string') {
|
||||||
channelsArea.value = node.channels;
|
channelsArea.value = node.channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateChannelsJson() {
|
function validateChannelsJson() {
|
||||||
if (!channelsHint) return;
|
if (!channelsHint) return;
|
||||||
if (modeSelect.value !== 'digital') { channelsHint.textContent = ''; return; }
|
if (!modeSelect || modeSelect.value !== 'digital') {
|
||||||
const raw = (channelsArea.value || '').trim();
|
channelsHint.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = (channelsArea && channelsArea.value || '').trim();
|
||||||
if (!raw || raw === '[]') {
|
if (!raw || raw === '[]') {
|
||||||
channelsHint.innerHTML = '<span style="color:#b45309;">Digital mode with no channels — no measurements will be emitted.</span>';
|
channelsHint.innerHTML = '<span style="color:#b45309;">Digital mode with no channels — no measurements will be emitted.</span>';
|
||||||
return;
|
return;
|
||||||
@@ -120,7 +126,7 @@
|
|||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (!Array.isArray(parsed)) throw new Error('must be an array');
|
if (!Array.isArray(parsed)) throw new Error('must be an array');
|
||||||
const missing = parsed
|
const missing = parsed
|
||||||
.map((c, i) => (c && c.key && c.type ? null : `entry ${i}: missing key or type`))
|
.map((c, i) => (c && c.key && c.type ? null : 'entry ' + i + ': missing key or type'))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
channelsHint.innerHTML = '<span style="color:#b45309;">' + missing.join('; ') + '</span>';
|
channelsHint.innerHTML = '<span style="color:#b45309;">' + missing.join('; ') + '</span>';
|
||||||
@@ -131,17 +137,24 @@
|
|||||||
channelsHint.innerHTML = '<span style="color:#b91c1c;">Invalid JSON: ' + e.message + '</span>';
|
channelsHint.innerHTML = '<span style="color:#b91c1c;">Invalid JSON: ' + e.message + '</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson);
|
|
||||||
|
|
||||||
// === Asset / logger / position placeholders (dynamic menus) ===
|
function applyMode(mode) {
|
||||||
const waitForMenuData = () => {
|
const isDigital = mode === 'digital';
|
||||||
if (window.EVOLV?.nodes?.measurement?.initEditor) {
|
if (analogBlock) analogBlock.style.display = isDigital ? 'none' : 'block';
|
||||||
window.EVOLV.nodes.measurement.initEditor(node);
|
if (digitalBlock) digitalBlock.style.display = isDigital ? 'block' : 'none';
|
||||||
} else {
|
if (modeHint) {
|
||||||
setTimeout(waitForMenuData, 50);
|
modeHint.textContent = isDigital
|
||||||
|
? 'msg.payload must be an OBJECT, e.g. {"temperature": 22.5, "humidity": 45}. Define each key below.'
|
||||||
|
: 'msg.payload must be a NUMBER (or numeric string). Configure scaling/smoothing below.';
|
||||||
|
}
|
||||||
|
validateChannelsJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modeSelect) modeSelect.addEventListener('change', (e) => applyMode(e.target.value));
|
||||||
|
if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson);
|
||||||
|
try { applyMode(initialMode); } catch (e) {
|
||||||
|
console.error('measurement: applyMode failed', e);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
waitForMenuData();
|
|
||||||
|
|
||||||
// === Smoothing method dropdown (analog only) ===
|
// === Smoothing method dropdown (analog only) ===
|
||||||
const smoothMethodSelect = document.getElementById('node-input-smooth_method');
|
const smoothMethodSelect = document.getElementById('node-input-smooth_method');
|
||||||
|
|||||||
Reference in New Issue
Block a user