Compare commits
15 Commits
fdfb9edf0d
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ba28b9cdf | ||
|
|
70acef22d5 | ||
|
|
93ea000734 | ||
|
|
d54cb66105 | ||
|
|
a3583a3edb | ||
|
|
98052a16e7 | ||
|
|
94b661658c | ||
|
|
43a5bf5468 | ||
|
|
2af30c0bd8 | ||
|
|
6953d6473e | ||
|
|
b8247fc755 | ||
|
|
b199663c77 | ||
|
|
518262ac98 | ||
|
|
9af42bdc4c | ||
|
|
a650ca4856 |
272
.gitignore
vendored
272
.gitignore
vendored
@@ -1,136 +1,136 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
40
CLAUDE.md
Normal file
40
CLAUDE.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# settler — Claude Code context
|
||||
|
||||
Secondary clarifier / sludge settling.
|
||||
Part of the [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV) wastewater-automation platform.
|
||||
|
||||
## S88 classification
|
||||
|
||||
| Level | Colour | Placement lane |
|
||||
|---|---|---|
|
||||
| **Unit** | `#50a8d9` | L4 |
|
||||
|
||||
## 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 **L4** (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 `#50a8d9` (Unit).
|
||||
|
||||
## Folder & File Layout
|
||||
|
||||
Every per-node file MUST use the folder name (`settler`) **exactly**, case-sensitive. Full rule: [`.claude/rules/node-architecture.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/node-architecture.md) in the EVOLV superproject.
|
||||
|
||||
| Path | Required name |
|
||||
|---|---|
|
||||
| Entry file | `settler.js` |
|
||||
| Editor HTML | `settler.html` |
|
||||
| Node adapter | `src/nodeClass.js` |
|
||||
| Domain logic | `src/specificClass.js` |
|
||||
| Editor JS modules | `src/editor/*.js` (extract when inline editor JS exceeds ~50 lines) |
|
||||
| Tests | `test/{basic,integration,edge}/*.test.js` |
|
||||
| Example flows | `examples/*.flow.json` |
|
||||
|
||||
|
||||
When adding new files, read the rule above first to avoid drift.
|
||||
52
CONTRACT.md
Normal file
52
CONTRACT.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# settler — Contract
|
||||
|
||||
Hand-maintained for Phase 6; the `## Inputs` table is generated from
|
||||
`src/commands/index.js` (see Phase 9 generator). Keep ≤ 80 lines.
|
||||
|
||||
## Inputs (msg.topic on Port 0)
|
||||
|
||||
| Canonical | Aliases (deprecated) | Payload | Effect |
|
||||
|---|---|---|---|
|
||||
| `data.influent` | `influent`, `setInfluent` | `{ F: number, C: number[13] }` — either field optional | Replaces influent flow and/or the 13-species concentration vector. Triggers `output-changed`, re-emits the 3-stream Fluent envelope. |
|
||||
| `child.register` | `registerChild` | `string` — the child node's Node-RED id | Resolves the child via `RED.nodes.getNode` and registers it through `childRegistrationUtils` at the supplied `msg.positionVsParent`. |
|
||||
|
||||
Aliases log a one-time deprecation warning the first time they fire.
|
||||
|
||||
## Outputs
|
||||
|
||||
- **Port 0 (process):** array of three Node-RED messages, each with
|
||||
`topic = 'Fluent'` and `payload = { inlet, F, C }`:
|
||||
- `inlet=0` — clarified effluent (particulate species 7–12 zeroed when `F_s > 0`).
|
||||
- `inlet=1` — surplus sludge (particulates concentrated by `F_in / F_s`).
|
||||
- `inlet=2` — return sludge (drawn by the downstream return pump up to `F_s`).
|
||||
Re-emitted whenever the upstream reactor fires `stateChange`, an
|
||||
operator pushes `data.influent`, or a child measurement updates `C_TS`.
|
||||
- **Port 1 (InfluxDB telemetry):** `msg.topic = config.general.name`,
|
||||
payload built by `outputUtils.formatMsg(..., 'influxdb')` from
|
||||
`getOutput()`. Carries `F_in`, `C_TS`, `F_eff`, `F_surplus`, `F_return`
|
||||
plus the flat measurements snapshot. Delta-compressed.
|
||||
- **Port 2 (registration):** at startup the node sends one
|
||||
`{ topic: 'child.register', payload: <node.id>, positionVsParent, distance }`
|
||||
to its parent.
|
||||
|
||||
## Events emitted by `source.measurements.emitter`
|
||||
|
||||
The `MeasurementContainer` fires `<type>.measured.<position>` whenever a
|
||||
matching series receives a new value. Settler re-emits incoming child
|
||||
measurements (e.g. `quantity (tss).measured.atequipment`) so its own
|
||||
parent can subscribe.
|
||||
|
||||
## Children accepted
|
||||
|
||||
| Software type | Position | Effect |
|
||||
|---|---|---|
|
||||
| `measurement` | any | Re-emit on `source.measurements`. `quantity (tss)` updates `C_TS` and triggers `output-changed`. |
|
||||
| `reactor` | `upstream` (warns otherwise) | Stored as `upstreamReactor`. Listener attached to the reactor's own `emitter` (NOT measurements) for `'stateChange'`; on fire, settler pulls `reactor.getEffluent` and copies `F_in` + `Cs_in`. Handles both array and single-envelope `getEffluent` shapes. |
|
||||
| `machine` | `downstream` | Stored as `returnPump`. Settler reads `returnPump.measurements.type('flow').variant('measured').position('atEquipment').getCurrentValue()` to determine `F_sr`. Sets `machineChild.upstreamSource = this`. |
|
||||
|
||||
## Parent relationship
|
||||
|
||||
Settler typically registers as `softwareType: 'settler'` with
|
||||
`positionVsParent: 'downstream'` against a reactor (the reactor's
|
||||
downstream stage). The downstream reactor consumes the three Fluent
|
||||
streams via `payload.inlet`.
|
||||
380
LICENSE
380
LICENSE
@@ -1,190 +1,190 @@
|
||||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the
|
||||
terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following
|
||||
notice immediately following the copyright notice for the Work:
|
||||
Licensed under the EUPL
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1.Definitions
|
||||
In this Licence, the following terms have the following meaning:
|
||||
— ‘The Licence’:this Licence.
|
||||
— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available
|
||||
as Source Code and also as Executable Code as the case may be.
|
||||
— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or
|
||||
modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in
|
||||
the country mentioned in Article 15.
|
||||
— ‘The Work’:the Original Work or its Derivative Works.
|
||||
— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and
|
||||
modify.
|
||||
— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by
|
||||
a computer as a program.
|
||||
— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence.
|
||||
— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to
|
||||
the creation of a Derivative Work.
|
||||
— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the
|
||||
Licence.
|
||||
— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating,
|
||||
transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential
|
||||
functionalities at the disposal of any other natural or legal person.
|
||||
|
||||
2.Scope of the rights granted by the Licence
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for
|
||||
the duration of copyright vested in the Original Work:
|
||||
— use the Work in any circumstance and for all usage,
|
||||
— reproduce the Work,
|
||||
— modify the Work, and make Derivative Works based upon the Work,
|
||||
— communicate to the public, including the right to make available or display the Work or copies thereof to the public
|
||||
and perform publicly, as the case may be, the Work,
|
||||
— distribute the Work or copies thereof,
|
||||
— lend and rent the Work or copies thereof,
|
||||
— sublicense rights in the Work or copies thereof.
|
||||
Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the
|
||||
applicable law permits so.
|
||||
In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed
|
||||
by law in order to make effective the licence of the economic rights here above listed.
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the
|
||||
extent necessary to make use of the rights granted on the Work under this Licence.
|
||||
|
||||
3.Communication of the Source Code
|
||||
The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as
|
||||
Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with
|
||||
each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to
|
||||
the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to
|
||||
distribute or communicate the Work.
|
||||
|
||||
4.Limitations on copyright
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the
|
||||
exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5.Obligations of the Licensee
|
||||
The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those
|
||||
obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to
|
||||
the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the
|
||||
Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work
|
||||
to carry prominent notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this
|
||||
Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless
|
||||
the Original Work is expressly distributed only under this version of the Licence — for example by communicating
|
||||
‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the
|
||||
Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both
|
||||
the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done
|
||||
under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed
|
||||
in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide
|
||||
a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available
|
||||
for as long as the Licensee continues to distribute or communicate the Work.
|
||||
Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names
|
||||
of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6.Chain of Authorship
|
||||
The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or
|
||||
licensed to him/her and that he/she has the power and authority to grant the Licence.
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or
|
||||
licensed to him/her and that he/she has the power and authority to grant the Licence.
|
||||
Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions
|
||||
to the Work, under the terms of this Licence.
|
||||
|
||||
7.Disclaimer of Warranty
|
||||
The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work
|
||||
and may therefore contain defects or ‘bugs’ inherent to this type of development.
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind
|
||||
concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or
|
||||
errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this
|
||||
Licence.
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
|
||||
|
||||
8.Disclaimer of Liability
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be
|
||||
liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the
|
||||
Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss
|
||||
of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However,
|
||||
the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
|
||||
|
||||
9.Additional agreements
|
||||
While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services
|
||||
consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10.Acceptance of the Licence
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window
|
||||
displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms
|
||||
and conditions.
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You
|
||||
by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution
|
||||
or Communication by You of the Work or copies thereof.
|
||||
|
||||
11.Information to the public
|
||||
In case of any Distribution or Communication of the Work by means of electronic communication by You (for example,
|
||||
by offering to download the Work from a remote location) the distribution channel or media (for example, a website)
|
||||
must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence
|
||||
and the way it may be accessible, concluded, stored and reproduced by the Licensee.
|
||||
|
||||
12.Termination of the Licence
|
||||
The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms
|
||||
of the Licence.
|
||||
Such a termination will not terminate the licences of any person who has received the Work from the Licensee under
|
||||
the Licence, provided such persons remain in full compliance with the Licence.
|
||||
|
||||
13.Miscellaneous
|
||||
Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the
|
||||
Work.
|
||||
If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or
|
||||
enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid
|
||||
and enforceable.
|
||||
The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of
|
||||
the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence.
|
||||
New versions of the Licence will be published with a unique version number.
|
||||
All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take
|
||||
advantage of the linguistic version of their choice.
|
||||
|
||||
14.Jurisdiction
|
||||
Without prejudice to specific agreement between parties,
|
||||
— any litigation resulting from the interpretation of this License, arising between the European Union institutions,
|
||||
bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice
|
||||
of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union,
|
||||
— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to
|
||||
the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
|
||||
|
||||
15.Applicable Law
|
||||
Without prejudice to specific agreement between parties,
|
||||
— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat,
|
||||
resides or has his registered office,
|
||||
— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside
|
||||
a European Union Member State.
|
||||
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
— 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) for works other than software
|
||||
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above licences without producing
|
||||
a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the
|
||||
covered Source Code from exclusive appropriation.
|
||||
All other changes or additions to this Appendix require the production of a new EUPL version.
|
||||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the
|
||||
terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following
|
||||
notice immediately following the copyright notice for the Work:
|
||||
Licensed under the EUPL
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1.Definitions
|
||||
In this Licence, the following terms have the following meaning:
|
||||
— ‘The Licence’:this Licence.
|
||||
— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available
|
||||
as Source Code and also as Executable Code as the case may be.
|
||||
— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or
|
||||
modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in
|
||||
the country mentioned in Article 15.
|
||||
— ‘The Work’:the Original Work or its Derivative Works.
|
||||
— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and
|
||||
modify.
|
||||
— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by
|
||||
a computer as a program.
|
||||
— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence.
|
||||
— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to
|
||||
the creation of a Derivative Work.
|
||||
— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the
|
||||
Licence.
|
||||
— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating,
|
||||
transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential
|
||||
functionalities at the disposal of any other natural or legal person.
|
||||
|
||||
2.Scope of the rights granted by the Licence
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for
|
||||
the duration of copyright vested in the Original Work:
|
||||
— use the Work in any circumstance and for all usage,
|
||||
— reproduce the Work,
|
||||
— modify the Work, and make Derivative Works based upon the Work,
|
||||
— communicate to the public, including the right to make available or display the Work or copies thereof to the public
|
||||
and perform publicly, as the case may be, the Work,
|
||||
— distribute the Work or copies thereof,
|
||||
— lend and rent the Work or copies thereof,
|
||||
— sublicense rights in the Work or copies thereof.
|
||||
Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the
|
||||
applicable law permits so.
|
||||
In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed
|
||||
by law in order to make effective the licence of the economic rights here above listed.
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the
|
||||
extent necessary to make use of the rights granted on the Work under this Licence.
|
||||
|
||||
3.Communication of the Source Code
|
||||
The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as
|
||||
Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with
|
||||
each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to
|
||||
the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to
|
||||
distribute or communicate the Work.
|
||||
|
||||
4.Limitations on copyright
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the
|
||||
exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5.Obligations of the Licensee
|
||||
The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those
|
||||
obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to
|
||||
the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the
|
||||
Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work
|
||||
to carry prominent notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this
|
||||
Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless
|
||||
the Original Work is expressly distributed only under this version of the Licence — for example by communicating
|
||||
‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the
|
||||
Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both
|
||||
the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done
|
||||
under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed
|
||||
in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide
|
||||
a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available
|
||||
for as long as the Licensee continues to distribute or communicate the Work.
|
||||
Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names
|
||||
of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6.Chain of Authorship
|
||||
The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or
|
||||
licensed to him/her and that he/she has the power and authority to grant the Licence.
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or
|
||||
licensed to him/her and that he/she has the power and authority to grant the Licence.
|
||||
Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions
|
||||
to the Work, under the terms of this Licence.
|
||||
|
||||
7.Disclaimer of Warranty
|
||||
The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work
|
||||
and may therefore contain defects or ‘bugs’ inherent to this type of development.
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind
|
||||
concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or
|
||||
errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this
|
||||
Licence.
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
|
||||
|
||||
8.Disclaimer of Liability
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be
|
||||
liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the
|
||||
Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss
|
||||
of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However,
|
||||
the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
|
||||
|
||||
9.Additional agreements
|
||||
While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services
|
||||
consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10.Acceptance of the Licence
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window
|
||||
displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms
|
||||
and conditions.
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You
|
||||
by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution
|
||||
or Communication by You of the Work or copies thereof.
|
||||
|
||||
11.Information to the public
|
||||
In case of any Distribution or Communication of the Work by means of electronic communication by You (for example,
|
||||
by offering to download the Work from a remote location) the distribution channel or media (for example, a website)
|
||||
must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence
|
||||
and the way it may be accessible, concluded, stored and reproduced by the Licensee.
|
||||
|
||||
12.Termination of the Licence
|
||||
The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms
|
||||
of the Licence.
|
||||
Such a termination will not terminate the licences of any person who has received the Work from the Licensee under
|
||||
the Licence, provided such persons remain in full compliance with the Licence.
|
||||
|
||||
13.Miscellaneous
|
||||
Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the
|
||||
Work.
|
||||
If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or
|
||||
enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid
|
||||
and enforceable.
|
||||
The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of
|
||||
the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence.
|
||||
New versions of the Licence will be published with a unique version number.
|
||||
All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take
|
||||
advantage of the linguistic version of their choice.
|
||||
|
||||
14.Jurisdiction
|
||||
Without prejudice to specific agreement between parties,
|
||||
— any litigation resulting from the interpretation of this License, arising between the European Union institutions,
|
||||
bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice
|
||||
of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union,
|
||||
— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to
|
||||
the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
|
||||
|
||||
15.Applicable Law
|
||||
Without prejudice to specific agreement between parties,
|
||||
— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat,
|
||||
resides or has his registered office,
|
||||
— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside
|
||||
a European Union Member State.
|
||||
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
— 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) for works other than software
|
||||
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above licences without producing
|
||||
a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the
|
||||
covered Source Code from exclusive appropriation.
|
||||
All other changes or additions to this Appendix require the production of a new EUPL version.
|
||||
|
||||
8
examples/README.md
Normal file
8
examples/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# settler Example Flows
|
||||
|
||||
Import-ready Node-RED examples for settler.
|
||||
|
||||
## Files
|
||||
- basic.flow.json
|
||||
- integration.flow.json
|
||||
- edge.flow.json
|
||||
6
examples/basic.flow.json
Normal file
6
examples/basic.flow.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"id":"settler_basic_tab","type":"tab","label":"settler basic","disabled":false,"info":"settler basic example"},
|
||||
{"id":"settler_basic_node","type":"settler","z":"settler_basic_tab","name":"settler basic","x":420,"y":180,"wires":[["settler_basic_dbg"]]},
|
||||
{"id":"settler_basic_inj","type":"inject","z":"settler_basic_tab","name":"basic trigger","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"ping","payload":"1","payloadType":"str","x":160,"y":180,"wires":[["settler_basic_node"]]},
|
||||
{"id":"settler_basic_dbg","type":"debug","z":"settler_basic_tab","name":"settler basic debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":660,"y":180,"wires":[]}
|
||||
]
|
||||
6
examples/edge.flow.json
Normal file
6
examples/edge.flow.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"id":"settler_edge_tab","type":"tab","label":"settler edge","disabled":false,"info":"settler edge example"},
|
||||
{"id":"settler_edge_node","type":"settler","z":"settler_edge_tab","name":"settler edge","x":420,"y":180,"wires":[["settler_edge_dbg"]]},
|
||||
{"id":"settler_edge_inj","type":"inject","z":"settler_edge_tab","name":"unknown topic","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"doesNotExist","payload":"x","payloadType":"str","x":170,"y":180,"wires":[["settler_edge_node"]]},
|
||||
{"id":"settler_edge_dbg","type":"debug","z":"settler_edge_tab","name":"settler edge debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":660,"y":180,"wires":[]}
|
||||
]
|
||||
6
examples/integration.flow.json
Normal file
6
examples/integration.flow.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"id":"settler_int_tab","type":"tab","label":"settler integration","disabled":false,"info":"settler integration example"},
|
||||
{"id":"settler_int_node","type":"settler","z":"settler_int_tab","name":"settler integration","x":420,"y":180,"wires":[["settler_int_dbg"]]},
|
||||
{"id":"settler_int_inj","type":"inject","z":"settler_int_tab","name":"registerChild","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"registerChild","payload":"example-child-id","payloadType":"str","x":170,"y":180,"wires":[["settler_int_node"]]},
|
||||
{"id":"settler_int_dbg","type":"debug","z":"settler_int_tab","name":"settler integration debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":680,"y":180,"wires":[]}
|
||||
]
|
||||
@@ -16,7 +16,10 @@
|
||||
"author": "P.R. van der Wilt",
|
||||
"main": "settler.js",
|
||||
"scripts": {
|
||||
"test": "node settler.js"
|
||||
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js",
|
||||
"wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md",
|
||||
"wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md",
|
||||
"wiki:all": "npm run wiki:contract && npm run wiki:datamodel"
|
||||
},
|
||||
"node-red": {
|
||||
"nodes": {
|
||||
|
||||
25
settler.html
25
settler.html
@@ -3,9 +3,11 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("settler", {
|
||||
category: "EVOLV",
|
||||
color: "#e4a363",
|
||||
color: "#8FAD3F",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
processOutputFormat: { value: "process" },
|
||||
dbaseOutputFormat: { value: "influxdb" },
|
||||
|
||||
enableLog: { value: false },
|
||||
logLevel: { value: "error" },
|
||||
@@ -55,6 +57,25 @@
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
|
||||
<h3>Output Formats</h3>
|
||||
<div class="form-row">
|
||||
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
||||
<select id="node-input-processOutputFormat" style="width:60%;">
|
||||
<option value="process">process</option>
|
||||
<option value="json">json</option>
|
||||
<option value="csv">csv</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
|
||||
<select id="node-input-dbaseOutputFormat" style="width:60%;">
|
||||
<option value="influxdb">influxdb</option>
|
||||
<option value="frost">frost</option>
|
||||
<option value="json">json</option>
|
||||
<option value="csv">csv</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Logger fields injected here -->
|
||||
<div id="logger-fields-placeholder"></div>
|
||||
|
||||
@@ -65,4 +86,4 @@
|
||||
|
||||
<script type="text/html" data-help-name="settler">
|
||||
<p>Settling tank</p>
|
||||
</script>
|
||||
</script>
|
||||
|
||||
50
settler.js
50
settler.js
@@ -1,26 +1,26 @@
|
||||
const nameOfNode = "settler"; // name of the node, should match file name and node type in Node-RED
|
||||
const nodeClass = require('./src/nodeClass.js'); // node class
|
||||
const { MenuManager } = require('generalFunctions');
|
||||
|
||||
|
||||
module.exports = function (RED) {
|
||||
// Register the node type
|
||||
RED.nodes.registerType(nameOfNode, function (config) {
|
||||
// Initialize the Node-RED node first
|
||||
RED.nodes.createNode(this, config);
|
||||
// Then create your custom class and attach it
|
||||
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
||||
});
|
||||
|
||||
const menuMgr = new MenuManager();
|
||||
|
||||
// Serve /settler/menu.js
|
||||
RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
|
||||
try {
|
||||
const script = menuMgr.createEndpoint(nameOfNode, ['logger', 'position']);
|
||||
res.type('application/javascript').send(script);
|
||||
} catch (err) {
|
||||
res.status(500).send(`// Error generating menu: ${err.message}`);
|
||||
}
|
||||
});
|
||||
const nameOfNode = "settler"; // name of the node, should match file name and node type in Node-RED
|
||||
const nodeClass = require('./src/nodeClass.js'); // node class
|
||||
const { MenuManager } = require('generalFunctions');
|
||||
|
||||
|
||||
module.exports = function (RED) {
|
||||
// Register the node type
|
||||
RED.nodes.registerType(nameOfNode, function (config) {
|
||||
// Initialize the Node-RED node first
|
||||
RED.nodes.createNode(this, config);
|
||||
// Then create your custom class and attach it
|
||||
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
||||
});
|
||||
|
||||
const menuMgr = new MenuManager();
|
||||
|
||||
// Serve /settler/menu.js
|
||||
RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
|
||||
try {
|
||||
const script = menuMgr.createEndpoint(nameOfNode, ['logger', 'position']);
|
||||
res.type('application/javascript').send(script);
|
||||
} catch (err) {
|
||||
res.status(500).send(`// Error generating menu: ${err.message}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
45
src/commands/handlers.js
Normal file
45
src/commands/handlers.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
// Handler functions for settler commands. Each handler receives:
|
||||
// source: the Settler domain instance.
|
||||
// msg: the Node-RED input message.
|
||||
// ctx: { node, RED, send, logger } — provided by BaseNodeAdapter.
|
||||
//
|
||||
// Settler accepts `child.register` (alias `registerChild`) on Port 0 input
|
||||
// to register measurement / reactor children, plus `data.influent` for
|
||||
// manual override. BaseNodeAdapter dispatches msg.topic through the
|
||||
// per-node registry — there is no implicit `child.register` handler in
|
||||
// the base, so it must be listed explicitly here.
|
||||
|
||||
function _logger(source, ctx) {
|
||||
return ctx?.logger || source?.logger || null;
|
||||
}
|
||||
|
||||
// Allows operators / upstream nodes to push an influent stream directly,
|
||||
// bypassing the reactor stateChange path. Payload mirrors the reactor's
|
||||
// `getEffluent` shape: { F, C } where C is the 13-species concentration
|
||||
// vector. Either field may be omitted to update only the other.
|
||||
exports.dataInfluent = (source, msg, ctx) => {
|
||||
const log = _logger(source, ctx);
|
||||
const p = msg?.payload;
|
||||
if (!p || typeof p !== 'object' || Array.isArray(p)) {
|
||||
log?.warn?.(`data.influent expects an object {F, C}; got ${typeof p}`);
|
||||
return;
|
||||
}
|
||||
if (typeof p.F === 'number' && Number.isFinite(p.F)) source.F_in = p.F;
|
||||
if (Array.isArray(p.C)) source.Cs_in = [...p.C];
|
||||
source.notifyOutputChanged();
|
||||
};
|
||||
|
||||
// Inbound child registration from a measurement (or reactor) child.
|
||||
// Ported from the legacy `case 'registerChild'` branch in nodeClass.
|
||||
exports.childRegister = (source, msg, ctx) => {
|
||||
const log = _logger(source, ctx);
|
||||
const childId = msg.payload;
|
||||
const childObj = ctx?.RED?.nodes?.getNode?.(childId);
|
||||
if (!childObj?.source) {
|
||||
log?.warn?.(`child.register skipped: missing child/source for id=${childId}`);
|
||||
return;
|
||||
}
|
||||
source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent, msg.distance);
|
||||
};
|
||||
27
src/commands/index.js
Normal file
27
src/commands/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
// settler command registry. Consumed by BaseNodeAdapter via
|
||||
// `static commands = require('./commands')`. Each descriptor maps a
|
||||
// canonical msg.topic to its handler; legacy names are listed under
|
||||
// `aliases` and emit a one-time deprecation warning at runtime.
|
||||
|
||||
const handlers = require('./handlers');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
topic: 'data.influent',
|
||||
aliases: ['influent', 'setInfluent'],
|
||||
payloadSchema: { type: 'any' },
|
||||
// Compound payload `{F, C: [...]}` — registry-level units normalisation is
|
||||
// skipped (the handler converts per-field internally; flow=m3/h, conc=mg/L).
|
||||
description: 'Push the influent stream (payload: {F: flow m3/h, C: [concentrations mg/L]}).',
|
||||
handler: handlers.dataInfluent,
|
||||
},
|
||||
{
|
||||
topic: 'child.register',
|
||||
aliases: ['registerChild'],
|
||||
payloadSchema: { type: 'string' },
|
||||
description: 'Register a child node (typically a measurement) with this settler.',
|
||||
handler: handlers.childRegister,
|
||||
},
|
||||
];
|
||||
120
src/nodeClass.js
120
src/nodeClass.js
@@ -1,103 +1,33 @@
|
||||
const { Settler } = require('./specificClass.js');
|
||||
const { configManager } = require('generalFunctions');
|
||||
'use strict';
|
||||
|
||||
const { BaseNodeAdapter } = require('generalFunctions');
|
||||
const Settler = require('./specificClass');
|
||||
const commands = require('./commands');
|
||||
|
||||
class nodeClass {
|
||||
/**
|
||||
* Node-RED node class for settler.
|
||||
* @param {object} uiConfig - Node-RED node configuration
|
||||
* @param {object} RED - Node-RED runtime API
|
||||
* @param {object} nodeInstance - Node-RED node instance
|
||||
* @param {string} nameOfNode - Name of the node
|
||||
*/
|
||||
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
|
||||
// Preserve RED reference for HTTP endpoints if needed
|
||||
this.node = nodeInstance;
|
||||
this.RED = RED;
|
||||
this.name = nameOfNode;
|
||||
this.source = null;
|
||||
// settler is event-driven on Port 0: the 3-stream Fluent envelope is
|
||||
// re-emitted whenever the upstream reactor fires stateChange or an
|
||||
// operator pushes data.influent. Port 1 (InfluxDB telemetry) reuses the
|
||||
// base `output-changed` pipeline via `getOutput()`. `tickInterval=null`
|
||||
// means BaseNodeAdapter installs no periodic loop — settling state has
|
||||
// no time-dependent integrator.
|
||||
class nodeClass extends BaseNodeAdapter {
|
||||
static DomainClass = Settler;
|
||||
static commands = commands;
|
||||
static tickInterval = null;
|
||||
static statusInterval = 1000;
|
||||
|
||||
this._loadConfig(uiConfig)
|
||||
this._setupClass();
|
||||
|
||||
this._attachInputHandler();
|
||||
this._registerChild();
|
||||
this._startTickLoop();
|
||||
this._attachCloseHandler();
|
||||
buildDomainConfig() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node-red input messages
|
||||
*/
|
||||
_attachInputHandler() {
|
||||
this.node.on('input', (msg, send, done) => {
|
||||
|
||||
switch (msg.topic) {
|
||||
case 'registerChild': {
|
||||
// Register this node as a parent of the child node
|
||||
const childId = msg.payload;
|
||||
const childObj = this.RED.nodes.getNode(childId);
|
||||
this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.source.logger.warn(`Unknown topic: ${msg.topic}`);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse node configuration
|
||||
* @param {object} uiConfig Config set in UI in node-red
|
||||
*/
|
||||
_loadConfig(uiConfig) {
|
||||
const cfgMgr = new configManager();
|
||||
this.config = cfgMgr.buildConfig('settler', uiConfig, this.node.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this node as a child upstream and downstream.
|
||||
* Delayed to avoid Node-RED startup race conditions.
|
||||
*/
|
||||
_registerChild() {
|
||||
setTimeout(() => {
|
||||
this.node.send([
|
||||
null,
|
||||
null,
|
||||
{ topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }
|
||||
]);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup settler class
|
||||
*/
|
||||
_setupClass() {
|
||||
|
||||
this.source = new Settler(this.config); // protect from reassignment
|
||||
this.node.source = this.source;
|
||||
}
|
||||
|
||||
_startTickLoop() {
|
||||
setTimeout(() => {
|
||||
this._tickInterval = setInterval(() => this._tick(), 1000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_tick(){
|
||||
this.node.send([this.source.getEffluent, null, null]);
|
||||
}
|
||||
|
||||
_attachCloseHandler() {
|
||||
this.node.on('close', (done) => {
|
||||
clearInterval(this._tickInterval);
|
||||
done();
|
||||
});
|
||||
_emitOutputs() {
|
||||
if (!this.source) return;
|
||||
const fluent = this.source.getEffluent;
|
||||
const raw = this.source.getOutput?.() || {};
|
||||
const cfg = this.source.config || this.config;
|
||||
const influxMsg = this._output.formatMsg(raw, cfg, 'influxdb');
|
||||
this.node.send([fluent, influxMsg, null]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nodeClass;
|
||||
module.exports = nodeClass;
|
||||
|
||||
@@ -1,144 +1,146 @@
|
||||
const { childRegistrationUtils, logger, MeasurementContainer, POSITIONS } = require('generalFunctions');
|
||||
const EventEmitter = require('events');
|
||||
'use strict';
|
||||
|
||||
class Settler {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
// EVOLV stuff
|
||||
this.logger = new logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, config.general.name);
|
||||
this.emitter = new EventEmitter();
|
||||
this.measurements = new MeasurementContainer();
|
||||
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
|
||||
const { BaseDomain, POSITIONS, statusBadge } = require('generalFunctions');
|
||||
|
||||
function cloneArray(values) {
|
||||
if (typeof structuredClone === 'function') return structuredClone(values);
|
||||
return Array.isArray(values) ? [...values] : values;
|
||||
}
|
||||
|
||||
// Settler — secondary clarifier / sludge separator (Unit level).
|
||||
// Splits influent into effluent, surplus sludge and return sludge based
|
||||
// on a TSS mass balance. State updates come from an upstream reactor
|
||||
// (stateChange → pull `getEffluent`) or operator-supplied influent via
|
||||
// the `data.influent` command. The 3-port Fluent stream is produced by
|
||||
// `getEffluent` and pushed onto Port 0 by the nodeClass.
|
||||
class Settler extends BaseDomain {
|
||||
static name = 'settler';
|
||||
|
||||
configure() {
|
||||
this.upstreamReactor = null;
|
||||
this.returnPump = null;
|
||||
|
||||
// state variables
|
||||
this.F_in = 0; // debit in
|
||||
this.Cs_in = new Array(13).fill(0); // Concentrations in
|
||||
this.C_TS = 2500; // Total solids concentration sludge
|
||||
this.F_in = 0;
|
||||
this.Cs_in = new Array(13).fill(0);
|
||||
this.C_TS = 2500;
|
||||
|
||||
this.router
|
||||
.onRegister('measurement', (child) => this._connectMeasurement(child))
|
||||
.onRegister('reactor', (child) => this._connectReactor(child))
|
||||
.onRegister('machine', (child) => this._connectMachine(child));
|
||||
}
|
||||
|
||||
// Three-stream output: effluent (inlet=0), surplus sludge (inlet=1),
|
||||
// return sludge (inlet=2). Downstream consumers (reactor inlets,
|
||||
// returnPump) read these by `payload.inlet`. F_s is clamped to F_in
|
||||
// to prevent negative effluent when X_TS_in/C_TS exceeds 1.
|
||||
get getEffluent() {
|
||||
// constrain flow to prevent negatives
|
||||
const F_s = Math.min((this.F_in * this.Cs_in[12]) / this.C_TS, this.F_in);
|
||||
const F_eff = this.F_in - F_s;
|
||||
|
||||
|
||||
let F_sr = 0;
|
||||
if (this.returnPump) {
|
||||
F_sr = Math.min(this.returnPump.measurements.type("flow").variant("measured").position(POSITIONS.AT_EQUIPMENT).getCurrentValue(), F_s);
|
||||
F_sr = Math.min(
|
||||
this.returnPump.measurements.type('flow').variant('measured').position(POSITIONS.AT_EQUIPMENT).getCurrentValue(),
|
||||
F_s,
|
||||
);
|
||||
}
|
||||
const F_so = F_s - F_sr;
|
||||
|
||||
// effluent
|
||||
const Cs_eff = structuredClone(this.Cs_in);
|
||||
if (F_s > 0) {
|
||||
Cs_eff[7] = 0;
|
||||
Cs_eff[8] = 0;
|
||||
Cs_eff[9] = 0;
|
||||
Cs_eff[10] = 0;
|
||||
Cs_eff[11] = 0;
|
||||
Cs_eff[12] = 0;
|
||||
}
|
||||
|
||||
// sludge
|
||||
const Cs_s = structuredClone(this.Cs_in);
|
||||
if (F_s > 0) {
|
||||
Cs_s[7] = this.F_in * this.Cs_in[7] / F_s;
|
||||
Cs_s[8] = this.F_in * this.Cs_in[8] / F_s;
|
||||
Cs_s[9] = this.F_in * this.Cs_in[9] / F_s;
|
||||
Cs_s[10] = this.F_in * this.Cs_in[10] / F_s;
|
||||
Cs_s[11] = this.F_in * this.Cs_in[11] / F_s;
|
||||
Cs_s[12] = this.F_in * this.Cs_in[12] / F_s;
|
||||
}
|
||||
const Cs_eff = cloneArray(this.Cs_in);
|
||||
if (F_s > 0) for (let i = 7; i <= 12; i++) Cs_eff[i] = 0;
|
||||
|
||||
const Cs_s = cloneArray(this.Cs_in);
|
||||
if (F_s > 0) for (let i = 7; i <= 12; i++) Cs_s[i] = this.F_in * this.Cs_in[i] / F_s;
|
||||
|
||||
const ts = Date.now();
|
||||
return [
|
||||
{ topic: "Fluent", payload: { inlet: 0, F: F_eff, C: Cs_eff }, timestamp: Date.now() },
|
||||
{ topic: "Fluent", payload: { inlet: 1, F: F_so, C: Cs_s }, timestamp: Date.now() },
|
||||
{ topic: "Fluent", payload: { inlet: 2, F: F_sr, C: Cs_s }, timestamp: Date.now() }
|
||||
{ topic: 'Fluent', payload: { inlet: 0, F: F_eff, C: Cs_eff }, timestamp: ts },
|
||||
{ topic: 'Fluent', payload: { inlet: 1, F: F_so, C: Cs_s }, timestamp: ts },
|
||||
{ topic: 'Fluent', payload: { inlet: 2, F: F_sr, C: Cs_s }, timestamp: ts },
|
||||
];
|
||||
}
|
||||
|
||||
registerChild(child, softwareType) {
|
||||
if(!child) {
|
||||
this.logger.error(`Invalid ${softwareType} child provided.`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (softwareType) {
|
||||
case "measurement":
|
||||
this.logger.debug(`Registering measurement child...`);
|
||||
this._connectMeasurement(child);
|
||||
break;
|
||||
case "reactor":
|
||||
this.logger.debug(`Registering reactor child...`);
|
||||
this._connectReactor(child);
|
||||
break;
|
||||
case "machine":
|
||||
this.logger.debug(`Registering machine child...`);
|
||||
this._connectMachine(child);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
||||
}
|
||||
}
|
||||
|
||||
_connectMeasurement(measurementChild) {
|
||||
const position = measurementChild.config.functionality.positionVsParent;
|
||||
const measurementType = measurementChild.config.asset.type;
|
||||
const eventName = `${measurementType}.measured.${position}`;
|
||||
const eventName = `${measurementType}.measured.${String(position).toLowerCase()}`;
|
||||
|
||||
// Register event listener for measurement updates
|
||||
measurementChild.measurements.emitter.on(eventName, (eventData) => {
|
||||
this.logger.debug(`${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
|
||||
|
||||
// Store directly in parent's measurement container
|
||||
this.measurements
|
||||
.type(measurementType)
|
||||
.variant("measured")
|
||||
.variant('measured')
|
||||
.position(position)
|
||||
.value(eventData.value, eventData.timestamp, eventData.unit);
|
||||
|
||||
this._updateMeasurement(measurementType, eventData.value, position, eventData);
|
||||
});
|
||||
}
|
||||
|
||||
// Reactor → settler integration: the reactor pushes a `stateChange` event
|
||||
// on its own emitter (NOT measurements.emitter), so router.onMeasurement
|
||||
// can't subscribe — we wire the listener manually here, mirroring the
|
||||
// pre-refactor `_connectReactor`. The settler pulls `getEffluent` rather
|
||||
// than receiving it pushed; reactor.getEffluent may return an array or a
|
||||
// single envelope (the 2026-03-02 bug fix preserved both shapes).
|
||||
_connectReactor(reactorChild) {
|
||||
if (reactorChild.config.functionality.positionVsParent != POSITIONS.UPSTREAM) {
|
||||
this.logger.warn("Reactor children of settlers should be upstream.");
|
||||
if (reactorChild.config.functionality.positionVsParent !== POSITIONS.UPSTREAM) {
|
||||
this.logger.warn('Reactor children of settlers should be upstream.');
|
||||
}
|
||||
|
||||
this.upstreamReactor = reactorChild;
|
||||
|
||||
reactorChild.emitter.on("stateChange", (_eventData) => {
|
||||
this.logger.debug(`State change of upstream reactor detected.`);
|
||||
const effluent = this.upstreamReactor.getEffluent[0];
|
||||
reactorChild.emitter.on('stateChange', () => {
|
||||
this.logger.debug('State change of upstream reactor detected.');
|
||||
const raw = this.upstreamReactor.getEffluent;
|
||||
const effluent = Array.isArray(raw) ? raw[0] : raw;
|
||||
this.F_in = effluent.payload.F;
|
||||
this.Cs_in = effluent.payload.C;
|
||||
this.notifyOutputChanged();
|
||||
});
|
||||
}
|
||||
|
||||
_connectMachine(machineChild) {
|
||||
if (machineChild.config.functionality.positionVsParent == POSITIONS.DOWNSTREAM) {
|
||||
if (machineChild.config.functionality.positionVsParent === POSITIONS.DOWNSTREAM) {
|
||||
machineChild.upstreamSource = this;
|
||||
this.returnPump = machineChild;
|
||||
return;
|
||||
}
|
||||
this.logger.warn(`Failed to register machine child.`);
|
||||
this.logger.warn('Failed to register machine child.');
|
||||
}
|
||||
|
||||
_updateMeasurement(measurementType, value, _position, _context) {
|
||||
switch(measurementType) {
|
||||
case "quantity (tss)":
|
||||
_updateMeasurement(measurementType, value /*, _position, _context */) {
|
||||
switch (measurementType) {
|
||||
case 'quantity (tss)':
|
||||
this.C_TS = value;
|
||||
break;
|
||||
|
||||
this.notifyOutputChanged();
|
||||
return;
|
||||
default:
|
||||
this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Telemetry snapshot for Port 1 (InfluxDB). Port 0 carries the 3-message
|
||||
// Fluent stream directly; this scalar view feeds dashboards.
|
||||
getOutput() {
|
||||
const streams = this.getEffluent;
|
||||
return {
|
||||
...this.measurements.getFlattenedOutput?.(),
|
||||
F_in: this.F_in,
|
||||
C_TS: this.C_TS,
|
||||
F_eff: streams[0].payload.F,
|
||||
F_surplus: streams[1].payload.F,
|
||||
F_return: streams[2].payload.F,
|
||||
};
|
||||
}
|
||||
|
||||
getStatusBadge() {
|
||||
if (this.F_in <= 0) return statusBadge.idle('no influent');
|
||||
const streams = this.getEffluent;
|
||||
const eff = streams[0].payload.F.toFixed(2);
|
||||
const sur = streams[1].payload.F.toFixed(2);
|
||||
return statusBadge.compose([`F_in=${this.F_in.toFixed(2)}`, `eff=${eff}`, `surplus=${sur}`], { fill: 'green', shape: 'dot' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Settler };
|
||||
module.exports = Settler;
|
||||
module.exports.Settler = Settler;
|
||||
|
||||
12
test/README.md
Normal file
12
test/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# settler Test Suite Layout
|
||||
|
||||
Required EVOLV layout:
|
||||
- basic/
|
||||
- integration/
|
||||
- edge/
|
||||
- helpers/
|
||||
|
||||
Baseline structure tests:
|
||||
- basic/structure-module-load.basic.test.js
|
||||
- integration/structure-examples.integration.test.js
|
||||
- edge/structure-examples-node-type.edge.test.js
|
||||
0
test/basic/.gitkeep
Normal file
0
test/basic/.gitkeep
Normal file
123
test/basic/specificClass.basic.test.js
Normal file
123
test/basic/specificClass.basic.test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const EventEmitter = require('events');
|
||||
const Settler = require('../../src/specificClass');
|
||||
|
||||
const NUM_SPECIES = 13;
|
||||
|
||||
function makeSettler() {
|
||||
return new Settler({
|
||||
general: { name: 'TestSettler', id: 'settler-test-1', logging: { enabled: false, logLevel: 'error' } },
|
||||
functionality: { softwareType: 'settler', positionVsParent: 'downstream' },
|
||||
});
|
||||
}
|
||||
|
||||
test('constructor sets default state', () => {
|
||||
const s = makeSettler();
|
||||
assert.equal(s.F_in, 0);
|
||||
assert.deepEqual(s.Cs_in, new Array(NUM_SPECIES).fill(0));
|
||||
assert.equal(s.C_TS, 2500);
|
||||
assert.equal(s.upstreamReactor, null);
|
||||
assert.equal(s.returnPump, null);
|
||||
});
|
||||
|
||||
test('getEffluent conserves total flow (mass balance)', () => {
|
||||
const s = makeSettler();
|
||||
s.F_in = 200;
|
||||
s.C_TS = 3000;
|
||||
const C = new Array(NUM_SPECIES).fill(5);
|
||||
C[12] = 2000;
|
||||
s.Cs_in = C;
|
||||
const [eff, sur, ret] = s.getEffluent;
|
||||
assert.equal(eff.topic, 'Fluent');
|
||||
assert.ok(Math.abs(eff.payload.F + sur.payload.F + ret.payload.F - 200) < 1e-6);
|
||||
for (let i = 7; i <= 12; i++) assert.equal(eff.payload.C[i], 0);
|
||||
});
|
||||
|
||||
test('getEffluent clamps F_s to F_in when X_TS exceeds C_TS', () => {
|
||||
const s = makeSettler();
|
||||
s.F_in = 100;
|
||||
s.C_TS = 1000;
|
||||
s.Cs_in = new Array(NUM_SPECIES).fill(10);
|
||||
s.Cs_in[12] = 5000;
|
||||
const [eff] = s.getEffluent;
|
||||
assert.equal(eff.payload.F, 0);
|
||||
});
|
||||
|
||||
test('reactor stateChange pulls effluent (preserves _connectReactor integration)', () => {
|
||||
const s = makeSettler();
|
||||
let outputChanges = 0;
|
||||
s.emitter.on('output-changed', () => outputChanges++);
|
||||
|
||||
const reactor = {
|
||||
config: { general: { name: 'r', id: 'r-1' }, functionality: { positionVsParent: 'upstream' } },
|
||||
emitter: new EventEmitter(),
|
||||
measurements: { emitter: new EventEmitter() },
|
||||
// Mirror the array shape the reactor produces in production.
|
||||
get getEffluent() {
|
||||
const C = new Array(NUM_SPECIES).fill(2);
|
||||
C[12] = 3500;
|
||||
return [{ topic: 'Fluent', payload: { inlet: 0, F: 150, C } }];
|
||||
},
|
||||
};
|
||||
s.router.dispatchRegister(reactor, 'reactor');
|
||||
reactor.emitter.emit('stateChange');
|
||||
|
||||
assert.equal(s.upstreamReactor, reactor);
|
||||
assert.equal(s.F_in, 150);
|
||||
assert.equal(s.Cs_in[12], 3500);
|
||||
assert.ok(outputChanges >= 1, 'reactor stateChange should trigger output-changed');
|
||||
});
|
||||
|
||||
test('reactor stateChange handles single-envelope getEffluent (not array)', () => {
|
||||
const s = makeSettler();
|
||||
const reactor = {
|
||||
config: { general: { name: 'r', id: 'r-1' }, functionality: { positionVsParent: 'upstream' } },
|
||||
emitter: new EventEmitter(),
|
||||
measurements: { emitter: new EventEmitter() },
|
||||
get getEffluent() {
|
||||
const C = new Array(NUM_SPECIES).fill(1);
|
||||
C[12] = 800;
|
||||
return { topic: 'Fluent', payload: { inlet: 0, F: 42, C } };
|
||||
},
|
||||
};
|
||||
s.router.dispatchRegister(reactor, 'reactor');
|
||||
reactor.emitter.emit('stateChange');
|
||||
|
||||
assert.equal(s.F_in, 42);
|
||||
assert.equal(s.Cs_in[12], 800);
|
||||
});
|
||||
|
||||
test('TSS measurement updates C_TS via _updateMeasurement', () => {
|
||||
const s = makeSettler();
|
||||
s._updateMeasurement('quantity (tss)', 7000);
|
||||
assert.equal(s.C_TS, 7000);
|
||||
});
|
||||
|
||||
test('downstream machine becomes returnPump', () => {
|
||||
const s = makeSettler();
|
||||
const pump = {
|
||||
config: { general: { name: 'pump', id: 'p-1' }, functionality: { positionVsParent: 'downstream' } },
|
||||
measurements: { emitter: new EventEmitter() },
|
||||
};
|
||||
s.router.dispatchRegister(pump, 'machine');
|
||||
assert.equal(s.returnPump, pump);
|
||||
assert.equal(pump.upstreamSource, s);
|
||||
});
|
||||
|
||||
test('getStatusBadge returns idle when F_in=0, green when flowing', () => {
|
||||
const s = makeSettler();
|
||||
const idle = s.getStatusBadge();
|
||||
assert.equal(idle.fill, 'blue');
|
||||
|
||||
s.F_in = 100;
|
||||
s.C_TS = 5000;
|
||||
const C = new Array(NUM_SPECIES).fill(10);
|
||||
C[12] = 3000;
|
||||
s.Cs_in = C;
|
||||
const active = s.getStatusBadge();
|
||||
assert.equal(active.fill, 'green');
|
||||
assert.ok(active.text.includes('F_in'));
|
||||
});
|
||||
8
test/basic/structure-module-load.basic.test.js
Normal file
8
test/basic/structure-module-load.basic.test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
test('settler module load smoke', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
require('../../settler.js');
|
||||
});
|
||||
});
|
||||
0
test/edge/.gitkeep
Normal file
0
test/edge/.gitkeep
Normal file
11
test/edge/structure-examples-node-type.edge.test.js
Normal file
11
test/edge/structure-examples-node-type.edge.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const flow = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../examples/basic.flow.json'), 'utf8'));
|
||||
|
||||
test('basic example includes node type settler', () => {
|
||||
const count = flow.filter((n) => n && n.type === 'settler').length;
|
||||
assert.equal(count >= 1, true);
|
||||
});
|
||||
0
test/helpers/.gitkeep
Normal file
0
test/helpers/.gitkeep
Normal file
0
test/integration/.gitkeep
Normal file
0
test/integration/.gitkeep
Normal file
23
test/integration/structure-examples.integration.test.js
Normal file
23
test/integration/structure-examples.integration.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const dir = path.resolve(__dirname, '../../examples');
|
||||
|
||||
function loadJson(file) {
|
||||
return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
||||
}
|
||||
|
||||
test('examples package exists for settler', () => {
|
||||
for (const file of ['README.md', 'basic.flow.json', 'integration.flow.json', 'edge.flow.json']) {
|
||||
assert.equal(fs.existsSync(path.join(dir, file)), true, file + ' missing');
|
||||
}
|
||||
});
|
||||
|
||||
test('example flows are parseable arrays for settler', () => {
|
||||
for (const file of ['basic.flow.json', 'integration.flow.json', 'edge.flow.json']) {
|
||||
const parsed = loadJson(file);
|
||||
assert.equal(Array.isArray(parsed), true);
|
||||
}
|
||||
});
|
||||
134
wiki/Home.md
Normal file
134
wiki/Home.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# settler
|
||||
|
||||
  
|
||||
|
||||
A `settler` models a secondary clarifier — the sludge-separation stage that sits downstream of a biological reactor. It receives the upstream reactor's effluent stream, performs a 13-species TSS mass balance, and splits the result into three Fluent envelopes: clarified effluent, surplus sludge, and return sludge. A downstream return pump (a `rotatingMachine` registered as `machine` / `downstream`) draws the return-sludge flow.
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. The shipped `examples/` folder ships stubs only — production-grade flows are still TODO. Treat this page as informative, not authoritative.
|
||||
|
||||
---
|
||||
|
||||
## At a glance
|
||||
|
||||
| Thing | Value |
|
||||
|:---|:---|
|
||||
| What it represents | Secondary clarifier / sludge settler — the gravity-separation stage between a biological reactor and its downstream sludge handling |
|
||||
| S88 level | Unit |
|
||||
| Use it when | You have a reactor whose effluent must be split into clarified water + return / surplus sludge by a TSS mass balance |
|
||||
| Don't use it for | Primary sedimentation (species 7–12 zeroing is wrong), generic mass-balance transforms (the 13-species ASM3 vector is hard-coded), single-tank SBRs that don't need a 3-stream split |
|
||||
| Children it accepts | `measurement` (any, but `quantity (tss)` is the only one that mutates state), `reactor` (upstream), `machine` (downstream — the return pump) |
|
||||
| Parents it talks to | Typically a downstream `reactor` — the three Fluent streams are routed by `payload.inlet` |
|
||||
|
||||
---
|
||||
|
||||
## How it fits
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
upstream[reactor<br/>upstream<br/>Unit]:::unit
|
||||
settler[settler<br/>Unit]:::unit
|
||||
downstream[reactor<br/>downstream<br/>Unit]:::unit
|
||||
return_pump[rotatingMachine<br/>return pump<br/>Equipment]:::equip
|
||||
tss[measurement<br/>quantity tss<br/>atequipment]:::ctrl
|
||||
|
||||
upstream -.stateChange.-> settler
|
||||
settler -->|Fluent inlet=0 effluent| downstream
|
||||
settler -->|Fluent inlet=1 surplus| downstream
|
||||
settler -->|Fluent inlet=2 return| return_pump
|
||||
return_pump -->|child.register downstream| settler
|
||||
tss -->|quantity tss.measured.atequipment| settler
|
||||
classDef unit fill:#50a8d9,color:#000
|
||||
classDef equip fill:#86bbdd,color:#000
|
||||
classDef ctrl fill:#a9daee,color:#000
|
||||
```
|
||||
|
||||
S88 colours are anchored in `.claude/rules/node-red-flow-layout.md`. The settler editor colour is currently `#e4a363` (orange) — tracked as drift in §16 of that rule; diagrams in this wiki use the correct Unit blue (`#50a8d9`).
|
||||
|
||||
---
|
||||
|
||||
## Try it — 1-minute demo
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The shipped examples (`examples/basic.flow.json`, `integration.flow.json`, `edge.flow.json`) are skeleton stubs — they create a settler node and a debug tap, but do not exercise the reactor → settler → pump chain. A proper Tier-1 / Tier-2 / Tier-3 example set is on the TODO list; until then this section walks the minimum stimulus.
|
||||
|
||||
Import the basic stub, deploy, then drive influent manually:
|
||||
|
||||
```bash
|
||||
curl -X POST -H 'Content-Type: application/json' \
|
||||
--data @nodes/settler/examples/basic.flow.json \
|
||||
http://localhost:1880/flow
|
||||
```
|
||||
|
||||
After deploy, send one inject:
|
||||
|
||||
| Topic | Payload | What it does |
|
||||
|:---|:---|:---|
|
||||
| `data.influent` | `{ "F": 1000, "C": [0,0,0,0,0,0,0,0,0,0,0,0,3000] }` | Pushes 1000 m³/h influent with 3000 mg/L total solids (index 12 = `X_TS`). Three Fluent envelopes appear on Port 0 immediately. |
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). Real flows (Tier-1 inject only, Tier-2 reactor + settler + pump, Tier-3 dashboard) are not yet shipped. See [Reference — Examples](Reference-Examples).
|
||||
|
||||
---
|
||||
|
||||
## The two things you'll send
|
||||
|
||||
| Topic | Aliases | Payload | What it does |
|
||||
|:---|:---|:---|:---|
|
||||
| `data.influent` | `influent`, `setInfluent` | `{F: number, C: number[13]}` — either field optional | Override the influent stream directly. Triggers a recompute of all three Fluent envelopes. |
|
||||
| `child.register` | `registerChild` | `string` (child node id) | Register a `measurement`, `reactor`, or `machine` child. Port 2 wiring does this automatically in normal flows. |
|
||||
|
||||
That is the entire input contract. Settler has no FSM, no setpoint, no startup sequence — it is a stateless transform on top of whatever the upstream reactor + measurement children push into it.
|
||||
|
||||
---
|
||||
|
||||
## What you'll see come out
|
||||
|
||||
Sample Port 0 messages (one push, three envelopes):
|
||||
|
||||
```json
|
||||
[
|
||||
{ "topic": "Fluent", "payload": { "inlet": 0, "F": 850.0, "C": [/*7-12 zeroed*/] }, "timestamp": 1715000000000 },
|
||||
{ "topic": "Fluent", "payload": { "inlet": 1, "F": 50.0, "C": [/*7-12 concentrated*/] }, "timestamp": 1715000000000 },
|
||||
{ "topic": "Fluent", "payload": { "inlet": 2, "F": 100.0, "C": [/*7-12 concentrated*/] }, "timestamp": 1715000000000 }
|
||||
]
|
||||
```
|
||||
|
||||
| `payload.inlet` | Meaning | Particulate species (indices 7–12) |
|
||||
|:---:|:---|:---|
|
||||
| `0` | Clarified effluent | zeroed when `F_s > 0` |
|
||||
| `1` | Surplus sludge (the fraction the return pump does not draw) | concentrated by `F_in / F_s` |
|
||||
| `2` | Return sludge (drawn by the downstream return pump, capped at `F_s`) | concentrated by `F_in / F_s` |
|
||||
|
||||
Mass balance invariant: `F_eff + F_surplus + F_return = F_in` (modulo float).
|
||||
|
||||
Port 1 (InfluxDB) is the scalar dashboard view — see [Reference — Contracts](Reference-Contracts#data-model--getoutput-shape). Port 2 carries the one-shot `child.register` upward at startup.
|
||||
|
||||
---
|
||||
|
||||
## Capability matrix
|
||||
|
||||
| Capability | Status | Notes |
|
||||
|:---|:---:|:---|
|
||||
| TSS mass-balance split (3 streams) | yes | `F_s = min(F_in * Cs[12] / C_TS, F_in)` — clamped to prevent negative effluent. |
|
||||
| Particulate zeroing in effluent | yes | Species 7–12 set to 0 in `inlet=0` when `F_s > 0`. |
|
||||
| Particulate concentration in sludge | yes | Species 7–12 scaled by `F_in / F_s` in `inlet=1` + `inlet=2`. |
|
||||
| Return-pump flow draw | yes | `F_sr = min(pump flow at equipment, F_s)`. Surplus = `F_s - F_sr`. |
|
||||
| `F_s` clamp to `F_in` | yes | Prevents negative effluent when `X_TS_in > C_TS`. |
|
||||
| Manual influent override | yes | `data.influent` lets ops supply `{F, C}` directly. |
|
||||
| Multiple reactor upstreams | no | Only one `upstreamReactor` slot; last registration wins. |
|
||||
| Stateful FSM | no | Stateless transform — recomputes on every trigger. |
|
||||
| Curve loading / drift / sequence-abort | n/a | Not applicable to a passive split. |
|
||||
|
||||
---
|
||||
|
||||
## Need more?
|
||||
|
||||
| Page | What you'll find |
|
||||
|:---|:---|
|
||||
| [Reference — Contracts](Reference-Contracts) | Topic registry, config schema, child registration filters |
|
||||
| [Reference — Architecture](Reference-Architecture) | Three-tier code map, reactor ↔ settler wiring (the load-bearing bit), lifecycle, output ports |
|
||||
| [Reference — Examples](Reference-Examples) | Shipped example flows (currently stubs) + the TODO list for production-grade demos |
|
||||
| [Reference — Limitations](Reference-Limitations) | When not to use, known limitations, open questions |
|
||||
|
||||
[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
|
||||
291
wiki/Reference-Architecture.md
Normal file
291
wiki/Reference-Architecture.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Reference — Architecture
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
|
||||
|
||||
Code structure for `settler`: the three-tier sandwich, the `src/` layout, the reactor ↔ settler wiring (the load-bearing bit), the lifecycle, and the output-port pipeline. For an intuitive overview, return to [Home](Home).
|
||||
|
||||
---
|
||||
|
||||
## Three-tier code layout
|
||||
|
||||
```
|
||||
nodes/settler/
|
||||
|
|
||||
+-- settler.js entry: RED.nodes.registerType('settler', ...) + /settler/menu.js admin route
|
||||
+-- settler.html editor form (Name, output formats, logger, position) — currently colour-drifted to #e4a363
|
||||
|
|
||||
+-- src/
|
||||
| nodeClass.js extends BaseNodeAdapter — domain wiring + Port 0/1 emit (~30 LOC)
|
||||
| specificClass.js extends BaseDomain — TSS split + child wiring (~140 LOC)
|
||||
| |
|
||||
| +-- commands/
|
||||
| index.js 2 topic descriptors: data.influent (+aliases), child.register
|
||||
| handlers.js pure handler functions
|
||||
```
|
||||
|
||||
Settler is small enough (~140 LOC of domain code) that no concern-split was needed; per the platform's MODULE_SPLIT contract a node only fans out into `src/<concern>/` modules when complexity demands it.
|
||||
|
||||
### Tier responsibilities
|
||||
|
||||
| Tier | File | What it owns | Touches `RED.*` |
|
||||
|:---|:---|:---|:---:|
|
||||
| entry | `settler.js` | Type registration + `/settler/menu.js` admin endpoint (proxies reactor's MenuManager). | yes |
|
||||
| nodeClass | `src/nodeClass.js` | `_emitOutputs()` only — calls `source.getEffluent` for the 3-msg Port-0 array and `source.getOutput()` for Port-1 InfluxDB telemetry. `tickInterval = null` (event-driven, no periodic loop). `statusInterval = 1000` for the badge. | yes |
|
||||
| specificClass | `src/specificClass.js` | `Settler.configure()` wires the `ChildRouter` for `measurement` / `reactor` / `machine` children. `getEffluent` is the mass-balance core. `getOutput` is the scalar Port-1 view. `getStatusBadge` renders the editor badge. | no |
|
||||
|
||||
`specificClass` does the work directly; there is no orchestration / concern layer between it and the math.
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The router-based registration uses `this.router.onRegister(...)` — verify against the latest BaseDomain contract when next touched.
|
||||
|
||||
---
|
||||
|
||||
## FSM
|
||||
|
||||
Not applicable. Settler is **stateless**. There is no FSM, no `state.*` member, no `executeSequence`, no movement / setpoint. Every trigger (`stateChange` from the upstream reactor, `data.influent` from an operator, or a `quantity (tss)` update from a measurement child) causes a fresh recompute of the three Fluent envelopes from the current runtime state, and the split immediately re-emits.
|
||||
|
||||
The status badge has two cosmetic branches only:
|
||||
|
||||
| Condition | Badge |
|
||||
|:---|:---|
|
||||
| `F_in <= 0` | `statusBadge.idle('no influent')` |
|
||||
| else | green dot, label `F_in=<n> eff=<n> surplus=<n>` |
|
||||
|
||||
There are no "protected states", no abort tokens, no allow-lists. All of that belongs to FSM-bearing nodes (`rotatingMachine`, `pumpingStation`, `machineGroupControl`, …).
|
||||
|
||||
---
|
||||
|
||||
## Reactor ↔ settler wiring — the load-bearing bit
|
||||
|
||||
This is the one piece of settler that needs care, because the reactor uses a different event channel than every other child type in EVOLV.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
reactor[upstream reactor]:::unit
|
||||
rEmit{reactor.emitter}
|
||||
rMeas{reactor.measurements.emitter}
|
||||
settler[settler._connectReactor]:::unit
|
||||
pull[upstreamReactor.getEffluent]
|
||||
apply[F_in = ...<br/>Cs_in = ...]
|
||||
notify[notifyOutputChanged()]
|
||||
|
||||
reactor --> rEmit
|
||||
reactor --> rMeas
|
||||
rEmit -- stateChange --> settler
|
||||
rMeas -. NOT used .- settler
|
||||
settler --> pull
|
||||
pull --> apply
|
||||
apply --> notify
|
||||
classDef unit fill:#50a8d9,color:#000
|
||||
```
|
||||
|
||||
Mechanism:
|
||||
|
||||
1. The reactor pushes its `stateChange` event on `reactor.emitter` — **not** on `reactor.measurements.emitter`. The standard `router.onMeasurement` subscription path therefore can't see it.
|
||||
2. `_connectReactor(reactorChild)` attaches the listener manually:
|
||||
|
||||
```js
|
||||
reactorChild.emitter.on('stateChange', () => {
|
||||
const raw = this.upstreamReactor.getEffluent;
|
||||
const effluent = Array.isArray(raw) ? raw[0] : raw;
|
||||
this.F_in = effluent.payload.F;
|
||||
this.Cs_in = effluent.payload.C;
|
||||
this.notifyOutputChanged();
|
||||
});
|
||||
```
|
||||
|
||||
3. `reactor.getEffluent` historically returned either an **array** (a 3-stream Fluent envelope, same shape settler itself emits) or a **single envelope** — the 2026-03-02 `_connectReactor` fix preserves both shapes via `Array.isArray(raw) ? raw[0] : raw`. If you change the reactor's effluent shape, this is the line to update.
|
||||
4. Position check: settler warns `Reactor children of settlers should be upstream.` if `positionVsParent !== 'upstream'`, but still stores the reactor. The wiring is not blocked — it just becomes the operator's problem to confirm intent.
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The settler-side pull-not-push semantics rely on `reactor.getEffluent` being a property getter (no arguments). Future reactor refactors that turn this into a parameterised method will need a coordinated change here.
|
||||
|
||||
---
|
||||
|
||||
## Return-pump wiring
|
||||
|
||||
`_connectMachine(machineChild)` is the second non-trivial registration path:
|
||||
|
||||
| Condition | Effect |
|
||||
|:---|:---|
|
||||
| `positionVsParent === 'downstream'` | Store as `this.returnPump`. Set `machineChild.upstreamSource = this` so the pump's own flow predictor can use settler's `inlet=2` envelope as its suction-side Fluent. |
|
||||
| else | Warn `Failed to register machine child.`, no storage. |
|
||||
|
||||
At each call to `getEffluent`, settler reads the pump's current measured flow:
|
||||
|
||||
```js
|
||||
F_sr = Math.min(
|
||||
this.returnPump.measurements.type('flow').variant('measured').position(POSITIONS.AT_EQUIPMENT).getCurrentValue(),
|
||||
F_s,
|
||||
);
|
||||
```
|
||||
|
||||
If no return pump is registered or its flow measurement is zero, `F_sr = 0` — in that case all separated sludge becomes surplus (`inlet=1`) and the return stream (`inlet=2`) is zero-flow.
|
||||
|
||||
---
|
||||
|
||||
## Measurement-child wiring
|
||||
|
||||
`_connectMeasurement(measurementChild)` is generic: it subscribes to `<type>.measured.<position>` on the child's `measurements.emitter` and:
|
||||
|
||||
1. Re-emits the value on settler's own `this.measurements` container (lets settler's own parent subscribe).
|
||||
2. Calls `_updateMeasurement(type, value, position, eventData)`.
|
||||
|
||||
`_updateMeasurement` currently recognises only one type:
|
||||
|
||||
| `measurementType` | Side-effect |
|
||||
|:---|:---|
|
||||
| `quantity (tss)` | Set `this.C_TS = value`. Trigger `notifyOutputChanged()`. |
|
||||
| anything else | Log an `error` (`Type '<type>' not recognized for measured update.`) — the re-emit still happened, just no settler-side state change. |
|
||||
|
||||
The `quantity (tss)` value is the **settler's own setpoint** for the target return-sludge concentration. The default is `2500` mg/L. Higher `C_TS` → less return + surplus, more effluent. Lower `C_TS` → more return + surplus.
|
||||
|
||||
---
|
||||
|
||||
## Mass-balance math (`getEffluent`)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
F_in[F_in m³/h]:::input
|
||||
Cs_in[Cs_in array 13<br/>mg/L per species]:::input
|
||||
C_TS[C_TS<br/>target return mg/L]:::input
|
||||
pumpFlow[returnPump.flow.measured.atequipment]:::input
|
||||
|
||||
F_in --> Fs[F_s = min(F_in × Cs_in[12] / C_TS, F_in)]
|
||||
Cs_in --> Fs
|
||||
C_TS --> Fs
|
||||
Fs --> F_eff[F_eff = F_in − F_s]
|
||||
Fs --> F_sr[F_sr = min(pumpFlow, F_s)]
|
||||
pumpFlow --> F_sr
|
||||
F_sr --> F_so[F_so = F_s − F_sr]
|
||||
|
||||
Cs_in --> CsEff[Cs_eff: copy then zero 7..12 if F_s > 0]
|
||||
Cs_in --> CsS[Cs_s: copy then scale 7..12 by F_in / F_s if F_s > 0]
|
||||
|
||||
F_eff --> envEff[envelope inlet=0]
|
||||
CsEff --> envEff
|
||||
F_so --> envSur[envelope inlet=1]
|
||||
CsS --> envSur
|
||||
F_sr --> envRet[envelope inlet=2]
|
||||
CsS --> envRet
|
||||
classDef input fill:#a9daee,color:#000
|
||||
```
|
||||
|
||||
Key facts:
|
||||
|
||||
- `F_s` is the **total separated sludge stream**. Mass-balance derivation: at steady state, `F_in * Cs_in[12] = F_s * C_TS` → `F_s = F_in * Cs_in[12] / C_TS`.
|
||||
- The clamp `min(..., F_in)` prevents `F_eff` going negative when `Cs_in[12] > C_TS` (i.e. the influent is denser than the target sludge concentration). Sub-rosa: the clamp masks the **upstream** problem; settler does not warn when the clamp fires — see [Reference — Limitations](Reference-Limitations#no-flow-balance-warning).
|
||||
- Species indices 7–12 are the ASM3 particulate species (`X_*`). Index 12 specifically is `X_TS` — the lumped total-suspended-solids surrogate the split is keyed off.
|
||||
- Soluble species 0–6 pass through unchanged in all three streams.
|
||||
- All three envelopes share a single `timestamp = Date.now()` — downstream consumers can rely on them being a coherent triple.
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle — what one trigger does
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant reactor as upstream reactor
|
||||
participant settler as settler (specificClass)
|
||||
participant nc as nodeClass
|
||||
participant pump as return pump child
|
||||
participant out as Port 0 / 1
|
||||
|
||||
reactor->>settler: emitter.emit('stateChange')
|
||||
settler->>reactor: read getEffluent
|
||||
reactor-->>settler: {F, C[13]} (or [{...}])
|
||||
settler->>settler: F_in = F · Cs_in = C
|
||||
settler->>settler: notifyOutputChanged()
|
||||
nc->>settler: read getEffluent (recompute)
|
||||
settler->>pump: read measurements.flow.measured.atequipment
|
||||
pump-->>settler: returnFlow (m³/h)
|
||||
settler->>settler: getEffluent — split into 3 envelopes
|
||||
settler-->>nc: [Fluent inlet=0, inlet=1, inlet=2]
|
||||
nc->>settler: read getOutput()
|
||||
settler-->>nc: {F_in, C_TS, F_eff, F_surplus, F_return, ...measurements}
|
||||
nc->>out: send([fluent, influxMsg, null])
|
||||
```
|
||||
|
||||
The three triggers that route through this lifecycle are:
|
||||
|
||||
| Trigger | Origin | Path |
|
||||
|:---|:---|:---|
|
||||
| Reactor `stateChange` | `reactor.emitter.emit('stateChange')` | `_connectReactor` listener → pull `getEffluent` → copy → `notifyOutputChanged` |
|
||||
| Operator `data.influent` | Inbound `msg.topic` | `commands/handlers.js#dataInfluent` → mutate `F_in` / `Cs_in` → `notifyOutputChanged` |
|
||||
| Measurement `quantity (tss)` | `measurementChild.measurements.emitter` | `_connectMeasurement` re-emit + `_updateMeasurement` → mutate `C_TS` → `notifyOutputChanged` |
|
||||
|
||||
`notifyOutputChanged` is BaseDomain's standard `output-changed` event. `BaseNodeAdapter` listens, calls `_emitOutputs()`, which produces the Port 0 / Port 1 messages.
|
||||
|
||||
---
|
||||
|
||||
## Output ports
|
||||
|
||||
| Port | Carries | Sample shape |
|
||||
|:---|:---|:---|
|
||||
| 0 (process) | **Array of three Node-RED messages**, each `{topic: 'Fluent', payload: {inlet, F, C}, timestamp}`. Re-emitted on every recompute. | See [Home — What you'll see come out](Home#what-youll-see-come-out). |
|
||||
| 1 (telemetry) | Single InfluxDB line-protocol payload built by `outputUtils.formatMsg(getOutput(), cfg, 'influxdb')`. Delta-compressed: only changed fields shipped. | `settler,id=settler_a F_in=1000,F_eff=850,F_surplus=50,F_return=100,C_TS=2500,...` |
|
||||
| 2 (register / control) | `null` from `_emitOutputs`. The one-shot `child.register` upward at startup goes through the BaseNodeAdapter init path, not `_emitOutputs`. | `{topic: 'child.register', payload: <node.id>, positionVsParent, distance}` (init only) |
|
||||
|
||||
Port-1 key shape from `getOutput()`:
|
||||
|
||||
| Key | Type | Source | Notes |
|
||||
|:---|:---|:---|:---|
|
||||
| `F_in` | number | `host.F_in` | Influent flow (m³/h). |
|
||||
| `C_TS` | number | `host.C_TS` | Target return-sludge concentration (mg/L). Default 2500. |
|
||||
| `F_eff` | number | `streams[0].payload.F` | Clarified effluent flow. |
|
||||
| `F_surplus` | number | `streams[1].payload.F` | Surplus sludge flow. |
|
||||
| `F_return` | number | `streams[2].payload.F` | Return sludge flow. |
|
||||
| `<type>.<variant>.<position>.<childId>` | varies | `measurements.getFlattenedOutput()` | Flattened snapshot of every measurement settler has seen (re-emitted from children). |
|
||||
|
||||
See [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) for the full InfluxDB layout.
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The `getOutput()` return shape has not been audited against an `_output-manifest.md` (the output-coverage rule). TODO: add the manifest and `test/basic/output-*.test.js` coverage in both populated and degraded states (no influent / no pump / no TSS measurement).
|
||||
|
||||
---
|
||||
|
||||
## Event sources
|
||||
|
||||
| Source | Where it fires | What it triggers |
|
||||
|:---|:---|:---|
|
||||
| `reactor.emitter` `'stateChange'` | Upstream reactor on every internal state advance | `_connectReactor` listener → pull `getEffluent` → recompute |
|
||||
| `measurementChild.measurements.emitter` `'<type>.measured.<position>'` | Any registered measurement child on a new sample | `_connectMeasurement` re-emit + `_updateMeasurement` switch |
|
||||
| Inbound `msg.topic = data.influent` | Node-RED input wire | `commands/handlers.js#dataInfluent` |
|
||||
| Inbound `msg.topic = child.register` | Node-RED input wire (rare; usually Port 2 wiring) | `commands/handlers.js#childRegister` |
|
||||
| `BaseDomain` `'output-changed'` | `notifyOutputChanged()` in domain | `nodeClass._emitOutputs()` |
|
||||
| `setInterval(statusInterval = 1000)` | `BaseNodeAdapter` | Re-render the status badge |
|
||||
|
||||
No per-second tick on the domain itself. `tickInterval = null` is explicit in `nodeClass`.
|
||||
|
||||
---
|
||||
|
||||
## Where to start reading
|
||||
|
||||
| If you're changing… | Read first |
|
||||
|:---|:---|
|
||||
| The TSS mass-balance math | `src/specificClass.js#getEffluent` (lines 37–62) |
|
||||
| Reactor ↔ settler wiring (stateChange listener, both-shape envelope handling) | `src/specificClass.js#_connectReactor` |
|
||||
| Return-pump wiring (the `machine` / `downstream` registration) | `src/specificClass.js#_connectMachine` |
|
||||
| Measurement re-emit + `C_TS` setpoint | `src/specificClass.js#_connectMeasurement` + `#_updateMeasurement` |
|
||||
| Operator-side influent override | `src/commands/handlers.js#dataInfluent` |
|
||||
| Port 0 (3-msg array) + Port 1 (InfluxDB) emit pipeline | `src/nodeClass.js#_emitOutputs` |
|
||||
| Status-badge text | `src/specificClass.js#getStatusBadge` |
|
||||
| Editor form / colour drift | `settler.html` (currently `color: '#e4a363'`; should be `#50a8d9`) |
|
||||
|
||||
---
|
||||
|
||||
## Related pages
|
||||
|
||||
| Page | Why |
|
||||
|:---|:---|
|
||||
| [Home](Home) | Intuitive overview |
|
||||
| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters |
|
||||
| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
|
||||
| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
|
||||
| [reactor wiki](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home) | The upstream parent — emits `stateChange` and exposes `getEffluent` |
|
||||
| [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) | The return-pump child — consumes `inlet=2` via `upstreamSource` |
|
||||
| [EVOLV — Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |
|
||||
176
wiki/Reference-Contracts.md
Normal file
176
wiki/Reference-Contracts.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Reference — Contracts
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). Content reflects `CONTRACT.md`, `src/commands/index.js`, and `generalFunctions/src/configs/settler.json` only.
|
||||
|
||||
Full topic contract, configuration schema, and child-registration filters for `settler`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/settler.json`. For an intuitive overview, return to the [Home](Home).
|
||||
|
||||
---
|
||||
|
||||
## Topic contract
|
||||
|
||||
The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire.
|
||||
|
||||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||||
|
||||
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||||
|---|---|---|---|---|
|
||||
| `data.influent` | `influent`, `setInfluent` | any | — | Push the influent stream (payload: {F: flow m3/h, C: [concentrations mg/L]}). |
|
||||
| `child.register` | `registerChild` | `string` | — | Register a child node (typically a measurement) with this settler. |
|
||||
|
||||
<!-- END AUTOGEN: topic-contract -->
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The autogen markers above will be populated by a future `npm run wiki:contract` tool. Until then the table is hand-maintained against `src/commands/index.js`.
|
||||
|
||||
### Mode / source / action allow-lists
|
||||
|
||||
**Not applicable.** Settler has no operational mode, no source allow-list, no action allow-list. Both topics are accepted unconditionally; payload-shape validation lives in the handler itself.
|
||||
|
||||
The `data.influent` handler validates:
|
||||
|
||||
```js
|
||||
if (!p || typeof p !== 'object' || Array.isArray(p)) {
|
||||
log?.warn?.(`data.influent expects an object {F, C}; got ${typeof p}`);
|
||||
return;
|
||||
}
|
||||
if (typeof p.F === 'number' && Number.isFinite(p.F)) source.F_in = p.F;
|
||||
if (Array.isArray(p.C)) source.Cs_in = [...p.C];
|
||||
```
|
||||
|
||||
Non-finite or non-numeric `F` is silently ignored. Non-array `C` is silently ignored. Either field may be omitted to update only the other. Negative `F` is **not** rejected — the downstream `getEffluent` math will produce nonsense but the node will not throw.
|
||||
|
||||
---
|
||||
|
||||
## Data model — `getOutput()` shape
|
||||
|
||||
Composed each tick by `src/specificClass.js` `getOutput()`. Port 0 carries the 3-message Fluent stream **directly** (not via `getOutput`); Port 1 (this snapshot) is the scalar dashboard view.
|
||||
|
||||
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
|
||||
|
||||
### Scalar keys
|
||||
|
||||
| Key | Type | Unit | Source | Notes |
|
||||
|:---|:---|:---|:---|:---|
|
||||
| `F_in` | number | m³/h | `host.F_in` | Influent flow. Default 0. |
|
||||
| `C_TS` | number | mg/L | `host.C_TS` | Target return-sludge concentration. Default 2500. Updated by `quantity (tss)` measurement child. |
|
||||
| `F_eff` | number | m³/h | `streams[0].payload.F` | Clarified effluent flow. |
|
||||
| `F_surplus` | number | m³/h | `streams[1].payload.F` | Surplus sludge flow (`F_s - F_sr`). |
|
||||
| `F_return` | number | m³/h | `streams[2].payload.F` | Return sludge flow (`min(pumpFlow, F_s)`). |
|
||||
|
||||
### Per-measurement keys
|
||||
|
||||
For every `(type, variant, position)` stored in `this.measurements` MeasurementContainer, the flattened output emits:
|
||||
|
||||
```
|
||||
<type>.<variant>.<position>.<childId>
|
||||
```
|
||||
|
||||
Position labels are normalised to lowercase. The trailing `<childId>` is the registering measurement child's id. Settler does not write its own measurements directly — every key in this group came from a registered child (the `_connectMeasurement` re-emit).
|
||||
|
||||
<!-- END AUTOGEN: data-model -->
|
||||
|
||||
### Status badge
|
||||
|
||||
`getStatusBadge()` in `src/specificClass.js`:
|
||||
|
||||
| Condition | Badge |
|
||||
|:---|:---|
|
||||
| `F_in <= 0` | `statusBadge.idle('no influent')` (grey ring) |
|
||||
| else | green dot, label `F_in=<n.nn> eff=<n.nn> surplus=<n.nn>` (m³/h, 2 dp) |
|
||||
|
||||
No state-symbol enumeration — settler has no FSM.
|
||||
|
||||
---
|
||||
|
||||
## Configuration schema — editor form to config keys
|
||||
|
||||
Source of truth: `generalFunctions/src/configs/settler.json` plus `settler.html`.
|
||||
|
||||
### General (`config.general`)
|
||||
|
||||
| Form field | Config key | Default | Notes |
|
||||
|:---|:---|:---|:---|
|
||||
| Name | `general.name` | `"Settler"` | Human-readable name. |
|
||||
| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
|
||||
| Default unit | `general.unit` | `null` | Default measurement unit. Currently unused by settler — child measurements carry their own units. |
|
||||
| Enable logging | `general.logging.enabled` | `true` | Master switch. |
|
||||
| Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. |
|
||||
|
||||
### Functionality (`config.functionality`)
|
||||
|
||||
| Form field | Config key | Default | Notes |
|
||||
|:---|:---|:---|:---|
|
||||
| (hidden) | `functionality.softwareType` | `settler` | Constant. Used by the parent's router as a registration filter. |
|
||||
| (hidden) | `functionality.role` | `Secondary settler for sludge separation` | Documentation string. |
|
||||
| Position vs parent | `functionality.positionVsParent` | `downstream` | One of `upstream` / `atEquipment` / `downstream`. Settler typically registers as `downstream` against an upstream reactor. |
|
||||
|
||||
### Node-RED-side (editor form, not domain config)
|
||||
|
||||
| Form field | Stored on | Default | Notes |
|
||||
|:---|:---|:---|:---|
|
||||
| Process Output Format | `nodeClass.processOutputFormat` | `process` | `process` / `json` / `csv`. Port-0 serialisation. |
|
||||
| Database Output Format | `nodeClass.dbaseOutputFormat` | `influxdb` | `influxdb` / `json` / `csv`. Port-1 serialisation. |
|
||||
|
||||
`buildDomainConfig()` returns `{}` — settler does not push any editor-derived values into the domain at start-up. All operational state is runtime (`F_in`, `Cs_in`, `C_TS`).
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The editor form (`settler.html`) is currently colour-drifted to `#e4a363` (orange); should be `#50a8d9` (Unit blue) per `.claude/rules/node-red-flow-layout.md` §16.
|
||||
|
||||
### Unit policy
|
||||
|
||||
| Quantity | Canonical (internal) | Output (Port 1) | Notes |
|
||||
|:---|:---|:---|:---|
|
||||
| Flow (`F_in`, `F_eff`, `F_surplus`, `F_return`) | m³/h | m³/h | No conversion — settler is unit-agnostic above the storage layer. |
|
||||
| Concentration (`C_TS`, `Cs_in[*]`) | mg/L | mg/L | Same. |
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). Settler does **not** declare a `requireUnitForTypes` policy via MeasurementContainer; verify against the general unit policy before relying on internal canonicalisation.
|
||||
|
||||
---
|
||||
|
||||
## Child registration
|
||||
|
||||
Source: `src/specificClass.js` `configure()` (the `this.router.onRegister(...)` chain) and the three `_connect*` methods.
|
||||
|
||||
| Software type | Filter | Wired to | Side-effect |
|
||||
|:---|:---|:---|:---|
|
||||
| `measurement` | any (no asset-type / position filter at register time) | `_connectMeasurement(child)` | Subscribes to `<type>.measured.<position>` on the child's `measurements.emitter`. Re-emits on settler's own MeasurementContainer (lets settler's parent see the value). `quantity (tss)` updates `C_TS`; anything else logs an `error` from `_updateMeasurement` but the re-emit still happened. |
|
||||
| `reactor` | `positionVsParent === 'upstream'` (warns otherwise but still registers) | `_connectReactor(child)` | Stored as `this.upstreamReactor`. Listener attached **manually** to `reactor.emitter` (NOT `measurements.emitter`) for `'stateChange'`; on fire, settler pulls `reactor.getEffluent` and copies `F_in` + `Cs_in`. Handles both array and single-envelope `getEffluent` shapes. |
|
||||
| `machine` | `positionVsParent === 'downstream'` (warns + skips otherwise) | `_connectMachine(child)` | Stored as `this.returnPump`. Settler reads `returnPump.measurements.type('flow').variant('measured').position('atEquipment').getCurrentValue()` to determine `F_sr`. Sets `machineChild.upstreamSource = this` so the pump can use settler's `inlet=2` Fluent as its suction-side context. |
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The `measurement` filter accepts any asset-type at register time; only the runtime `_updateMeasurement` switch acts on `quantity (tss)`. Other measurement types are silently re-emitted but not consumed. TODO: decide whether this is desired (pass-through telemetry) or a contract gap.
|
||||
|
||||
### No virtual children
|
||||
|
||||
Unlike `rotatingMachine`, settler does **not** auto-register any virtual measurement children. Every measurement must come from an explicitly wired child node.
|
||||
|
||||
---
|
||||
|
||||
## Parent relationship
|
||||
|
||||
Settler typically registers as `softwareType: 'settler'` with `positionVsParent: 'downstream'` against a reactor (the reactor's downstream stage). The downstream reactor consumes the three Fluent streams via `payload.inlet`:
|
||||
|
||||
| `inlet` | Consumer expectation |
|
||||
|:---|:---|
|
||||
| `0` (clarified effluent) | Routed onward to the next process unit. |
|
||||
| `1` (surplus sludge) | Typically routed to a sludge-handling process (digestion, thickening, dewatering). |
|
||||
| `2` (return sludge) | Drawn back to a reactor inlet or the head of the biological train. |
|
||||
|
||||
Multi-reactor settlers are **not supported** — `this.upstreamReactor` is a single slot; the last `child.register` call wins.
|
||||
|
||||
---
|
||||
|
||||
## Related pages
|
||||
|
||||
| Page | Why |
|
||||
|:---|:---|
|
||||
| [Home](Home) | Intuitive overview |
|
||||
| [Reference — Architecture](Reference-Architecture) | Code map, reactor ↔ settler wiring, mass-balance math |
|
||||
| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
|
||||
| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
|
||||
| [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
|
||||
| [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |
|
||||
139
wiki/Reference-Examples.md
Normal file
139
wiki/Reference-Examples.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Reference — Examples
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The shipped example flows are **stub level**: each is a 4-node skeleton (tab + node + inject + debug) that proves the node loads in Node-RED but does **not** exercise the reactor → settler → return-pump chain or the TSS mass-balance math. Production-grade examples are TODO. See [Reference — Limitations — Example flows](Reference-Limitations#example-flows-are-stub-level).
|
||||
|
||||
---
|
||||
|
||||
## Shipped examples
|
||||
|
||||
| File | Tier | Dependencies | What it shows |
|
||||
|:---|:---:|:---|:---|
|
||||
| `basic.flow.json` | 1 (stub) | EVOLV only | Loads a single settler node, wires a `ping` inject to its input, taps Port 0 (only) to a debug node. Inject payload is the string `"1"` — will be rejected by `data.influent`'s payload validator (warn logged). Useful only to verify the node type registers. |
|
||||
| `integration.flow.json` | 2 (stub) | EVOLV only | Same shape; inject sends `topic = registerChild`, payload `example-child-id`. The lookup will fail (no such node) and log a warn. Does **not** exercise reactor or pump wiring. |
|
||||
| `edge.flow.json` | 3 (stub) | EVOLV only | Same shape; inject sends `topic = doesNotExist`. Verifies the registry rejects unknown topics. |
|
||||
|
||||
### Status
|
||||
|
||||
| Aspect | State |
|
||||
|:---|:---|
|
||||
| Loads in Node-RED on deploy | yes |
|
||||
| Drives `data.influent` with a valid payload | no |
|
||||
| Drives the reactor → settler → return-pump chain | no |
|
||||
| Shows the 3-stream Fluent split on Port 0 | no |
|
||||
| Has a dashboard tier | no |
|
||||
| Validated against a live Node-RED instance | no |
|
||||
|
||||
---
|
||||
|
||||
## Loading a flow
|
||||
|
||||
### Via the editor
|
||||
|
||||
1. Open the Node-RED editor at `http://localhost:1880`.
|
||||
2. Menu → Import → drag the JSON file.
|
||||
3. Click Deploy.
|
||||
|
||||
### Via the Admin API
|
||||
|
||||
```bash
|
||||
curl -X POST -H 'Content-Type: application/json' \
|
||||
--data @nodes/settler/examples/basic.flow.json \
|
||||
http://localhost:1880/flows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TODO — production-grade example set
|
||||
|
||||
The matching `rotatingMachine` repo ships three tiers; settler needs the same. Tracking placeholder:
|
||||
|
||||
| Tier | Proposed filename | What it should show |
|
||||
|:---|:---|:---|
|
||||
| 1 | `01 - Basic Manual Influent.json` | Single settler + inject sending `data.influent` with a realistic `{F, C}` payload. Three debug taps (one per Port 0 stream by `payload.inlet`). Operator can vary F and watch the split rebalance. |
|
||||
| 2 | `02 - Reactor and Return Pump.json` | One `reactor` (upstream) + one `settler` + one `rotatingMachine` (return pump, downstream). Auto-registration via Port 2. Drive the reactor with an inject; settler should re-split on every reactor `stateChange`. |
|
||||
| 3 | `03 - Dashboard Visualization.json` | FlowFuse Dashboard 2.0 page: F_in / F_eff / F_surplus / F_return trend chart, C_TS gauge, status badges per stream. Required: `@flowfuse/node-red-dashboard` installed. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshots needed** once the production-grade examples land. Save as `wiki/_partial-screenshots/settler/01-basic-editor.png`, `02-reactor-pump-editor.png`, `03-dashboard-rendered.png`, ≤ 200 KB each.
|
||||
|
||||
---
|
||||
|
||||
## What the basic example would do (sketch — not yet shipped)
|
||||
|
||||
Operator workflow once a real Tier-1 ships:
|
||||
|
||||
1. Deploy the flow.
|
||||
2. Send `data.influent` with payload:
|
||||
|
||||
```json
|
||||
{ "F": 1000, "C": [0,0,0,0,0,0,0,30,80,400,200,80,3000] }
|
||||
```
|
||||
|
||||
Twelve soluble species at low concentrations + index 12 `X_TS = 3000` mg/L.
|
||||
3. Observe three Port-0 messages arrive simultaneously, each with `topic = "Fluent"` and `payload.inlet` ∈ {0, 1, 2}.
|
||||
4. With default `C_TS = 2500` mg/L:
|
||||
- `F_s = 1000 * 3000 / 2500 = 1200` — but clamped to `F_in = 1000`. **The clamp fires** — this is the input edge case [Reference — Limitations — no-flow-balance warning](Reference-Limitations#no-flow-balance-warning) refers to.
|
||||
- `F_eff = 0`. The clarified-effluent envelope carries zero flow but still has its `C` vector (with species 7–12 zeroed).
|
||||
- `F_sr` is 0 (no pump wired) → `F_surplus = 1000`, `F_return = 0`.
|
||||
5. Send `data.influent` with `X_TS = 1500` mg/L instead. Now `F_s = 600`, `F_eff = 400`, `F_surplus = 600`, `F_return = 0`. Realistic split.
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). The clamp-fires behaviour above is intended (per code comment in `getEffluent`) but produces no operator-visible warning. Tracked.
|
||||
|
||||
---
|
||||
|
||||
## Docker compose snippet
|
||||
|
||||
To bring up Node-RED + InfluxDB with EVOLV nodes pre-loaded:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml (extract)
|
||||
services:
|
||||
nodered:
|
||||
build: ./docker/nodered
|
||||
ports: ['1880:1880']
|
||||
volumes:
|
||||
- ./docker/nodered/data:/data/evolv
|
||||
influxdb:
|
||||
image: influxdb:2.7
|
||||
ports: ['8086:8086']
|
||||
```
|
||||
|
||||
Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/docker-compose.yml).
|
||||
|
||||
---
|
||||
|
||||
## Debug recipes
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). Recipes below are grounded in the source; the symptom-side has not been confirmed against a live deployment.
|
||||
|
||||
| Symptom | First thing to check | Where to look |
|
||||
|:---|:---|:---|
|
||||
| `F_eff` negative or `NaN` | `C_TS` is zero or `Cs_in[12]` is huge. The `F_s` clamp should prevent negatives — confirm the clamp `min(..., F_in)` is present. Likely the clamp fires but masks a deeper input problem. | `src/specificClass.js#getEffluent` line `const F_s = Math.min(...)`. |
|
||||
| Settler never updates after reactor changes | Reactor child is not on `'upstream'` position (warn logged but registration proceeds), or the listener is attached to the wrong emitter. | `_connectReactor` — listens on `reactor.emitter`, **NOT** `reactor.measurements.emitter`. |
|
||||
| Return-sludge flow stays at 0 | `returnPump.measurements.type('flow').variant('measured').position('atEquipment')` has no current value. Wire a flow measurement child on the pump (with `asset.type='flow'`, `positionVsParent='atEquipment'`). | `_connectMachine`, pump's MeasurementContainer chain. |
|
||||
| Three Fluent envelopes do not arrive at the downstream consumer | `payload.inlet` selector on the downstream reactor / pump mismatches (0 = effluent, 1 = surplus, 2 = return). | The downstream consumer's inlet routing. |
|
||||
| `quantity (tss)` updates don't change `C_TS` | Measurement child's `asset.type` must be the literal string `"quantity (tss)"` (with the space + parenthesised "tss"). | `src/specificClass.js#_updateMeasurement` switch case. |
|
||||
| `data.influent` inject is silently dropped | Payload must be an object `{F, C}`. A string, number, or array logs a warn (`data.influent expects an object {F, C}; got <type>`) and short-circuits. | `src/commands/handlers.js#dataInfluent`. |
|
||||
| Settler logs an `error` `Type '<x>' not recognized for measured update.` | A measurement child has registered with an `asset.type` settler doesn't recognise. The re-emit still happened — the error is about the absence of a `_updateMeasurement` switch case. Currently only `quantity (tss)` mutates state. | `_updateMeasurement`. |
|
||||
| `child.register` topic logs `child.register skipped: missing child/source for id=<x>` | The given node id doesn't resolve to a Node-RED node with a `.source` (i.e. an EVOLV domain). Verify the id is correct and the target node has already been deployed. | `src/commands/handlers.js#childRegister`. |
|
||||
| Status badge stuck on `idle: no influent` | `F_in <= 0`. Either no reactor has fired `stateChange` yet, or `data.influent` has not been sent with a positive `F`. | `src/specificClass.js#getStatusBadge`. |
|
||||
|
||||
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors.
|
||||
|
||||
---
|
||||
|
||||
## Related pages
|
||||
|
||||
| Page | Why |
|
||||
|:---|:---|
|
||||
| [Home](Home) | Intuitive overview |
|
||||
| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters |
|
||||
| [Reference — Architecture](Reference-Architecture) | Code map, reactor ↔ settler wiring, mass-balance math |
|
||||
| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
|
||||
| [reactor — Examples](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Reference-Examples) | The upstream parent — how to drive `stateChange` |
|
||||
| [EVOLV — Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where settler fits in a larger plant |
|
||||
116
wiki/Reference-Limitations.md
Normal file
116
wiki/Reference-Limitations.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Reference — Limitations
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). What `settler` does not do, current rough edges, and open questions. Open items live in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` in the superproject.
|
||||
|
||||
---
|
||||
|
||||
## When you would not use this node
|
||||
|
||||
| Scenario | Use instead |
|
||||
|:---|:---|
|
||||
| Primary sedimentation upstream of biological treatment | The species 7–12 zeroing in the effluent stream is wrong for primary sludge (the soluble / particulate split is different). Model as a separate node. |
|
||||
| Generic mass-balance transform | The 13-species ASM3 concentration vector is hard-coded; `Cs[12]` (`X_TS`) is the only species the split is keyed off. Not a fit for arbitrary stream-splitting. |
|
||||
| Single-tank SBR with no separation stage | The 3-stream output expects a downstream consumer that routes by `payload.inlet`. A direct reactor → reactor wire is lighter. |
|
||||
| A reactor — you want to model biological transformation, not separation | `reactor` (settler is a passive separator, not a biological process). |
|
||||
| A return-pump itself — you want to model the pump's behaviour | `rotatingMachine` (settler reads the pump's measured flow but does not control it). |
|
||||
|
||||
---
|
||||
|
||||
## Known limitations
|
||||
|
||||
### Example flows are stub level
|
||||
|
||||
The three shipped flows (`basic.flow.json`, `integration.flow.json`, `edge.flow.json`) are 4-node skeletons (tab + node + inject + debug). They prove the node loads in Node-RED but do not exercise the reactor → settler → pump chain, do not drive `data.influent` with a valid payload, and do not have a dashboard tier. Production-grade examples are TODO — see [Reference — Examples — TODO](Reference-Examples#todo--production-grade-example-set).
|
||||
|
||||
### Editor colour drift
|
||||
|
||||
`settler.html` declares `color: '#e4a363'` (orange). The S88 Unit level requires `#50a8d9` (blue). The placement-rule registry (`.claude/rules/node-red-flow-layout.md` §14) already maps `settler` to the `UN` lane regardless of editor colour, so demos lay out correctly; the cosmetic mismatch is tracked in §16 of the same rule and is on the colour-cleanup list. The wiki diagrams use the correct blue.
|
||||
|
||||
### Single-reactor upstream slot
|
||||
|
||||
`this.upstreamReactor` is a single slot. Registering a second `reactor` child with `positionVsParent='upstream'` silently overwrites the first — the listener on the previous reactor's `emitter` is not detached, so it keeps firing into a settler that will pull effluent from the new reactor instead. Tracked.
|
||||
|
||||
### `X_TS` index is hard-coded
|
||||
|
||||
The mass balance uses `Cs_in[12]` (the ASM3 `X_TS` lumped solids species) as the surrogate for total suspended solids. Any change to the species ordering in the upstream reactor breaks settler. The coupling is not documented in the schema — it lives only in the `getEffluent` math and the `_updateMeasurement` switch case for `quantity (tss)`. Tracked.
|
||||
|
||||
### No flow-balance warning
|
||||
|
||||
When influent solids exceed the target return concentration (`Cs_in[12] > C_TS`), `F_s` is clamped to `F_in` and clarified effluent drops to zero. This is mathematically correct but masks an upstream problem (overloaded reactor, miscalibrated `C_TS` setpoint). The clamp fires silently — no warn, no badge change beyond the eventual `F_in <= 0` idle state. Operator must monitor `F_eff` directly. Tracked.
|
||||
|
||||
### `quantity (tss)` measurement passes through but doesn't validate
|
||||
|
||||
`_connectMeasurement` re-emits every measurement type, but `_updateMeasurement` only acts on `quantity (tss)`. Other types log an `error` (`Type '<x>' not recognized for measured update.`) but the re-emit already happened — the parent of settler still sees the value. Whether this is desired (settler acts as telemetry pass-through) or a contract gap is unresolved.
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). Open question: should `_connectMeasurement` filter by asset-type at register time and reject non-`quantity (tss)` children, or continue accepting everything as pass-through telemetry?
|
||||
|
||||
### No output manifest / no degraded-state coverage
|
||||
|
||||
Per the platform output-coverage rule (`.claude/rules/output-coverage.md`), every node needs a `test/_output-manifest.md` enumerating every Port 0 / 1 / 2 key and a `test/basic/output-*.test.js` exercising each one in both populated **and** degraded states. Settler has neither. The most likely degraded-state crash points:
|
||||
|
||||
- Port 0 emitted before any reactor `stateChange` — `F_in = 0`, `Cs_in = [0...]`, the three envelopes carry zero flow but valid (zero) `C` arrays. Should not crash a downstream consumer, but un-tested.
|
||||
- Port 0 with `returnPump` registered but no flow measurement landed yet — `returnPump.measurements.type('flow').variant('measured').position('atEquipment').getCurrentValue()` returns `undefined` → `Math.min(undefined, F_s) = NaN`. `F_sr` becomes `NaN` → both `inlet=1` and `inlet=2` carry `NaN` flow.
|
||||
|
||||
Tracked. TODO before trial-readiness: add the manifest, tests, and `null`-flow-measurement handling.
|
||||
|
||||
### `reactor.getEffluent` shape coupling
|
||||
|
||||
`_connectReactor` does `Array.isArray(raw) ? raw[0] : raw` to absorb both the older single-envelope shape and the newer 3-stream array shape of `reactor.getEffluent`. The 2026-03-02 fix is the only thing keeping the older shape alive in production. If `reactor.getEffluent` ever returns a 3-stream array and settler should consume `inlet=0` specifically, the current `raw[0]` selector works by accident — it picks the first envelope regardless of inlet number. Open question whether to make the selection inlet-aware.
|
||||
|
||||
### Stateful telemetry — no decay / no TTL
|
||||
|
||||
The MeasurementContainer holds the last-known value of every re-emitted measurement forever. If a child stops publishing, the stale value persists on Port 1 indefinitely. There is no TTL, no `data.clear-measurement` topic, and no health flag like `rotatingMachine`'s `predictionQuality`. Open question.
|
||||
|
||||
---
|
||||
|
||||
## Open questions (tracked)
|
||||
|
||||
| Question | Where it lives |
|
||||
|:---|:---|
|
||||
| Filter `_connectMeasurement` by asset-type at register time? | Internal — not yet ticketed |
|
||||
| Multi-reactor upstream support — teardown ordering, listener detach | Internal |
|
||||
| Flow-balance warning when `F_s` clamp fires | Internal |
|
||||
| Inlet-aware selection in `_connectReactor` shape handling | Internal |
|
||||
| Measurement TTL / staleness flag | Internal |
|
||||
| Production-grade example flows (Tier 1 / 2 / 3) | `.agents/improvements/IMPROVEMENTS_BACKLOG.md` |
|
||||
| Output manifest + degraded-state tests | `.claude/rules/output-coverage.md` (platform-wide rule) |
|
||||
| Editor colour cleanup (`#e4a363` → `#50a8d9`) | `.claude/rules/node-red-flow-layout.md` §16 |
|
||||
|
||||
---
|
||||
|
||||
## Migration notes
|
||||
|
||||
> [!NOTE]
|
||||
> Pending full node review (2026-05). No structural migrations have been performed on settler since the AssetResolver refactor of rotatingMachine; the notes below document the one historical fix on record.
|
||||
|
||||
### From pre-2026-03-02 `_connectReactor`
|
||||
|
||||
Before 2026-03-02 `_connectReactor` assumed `reactor.getEffluent` always returned an array, and indexed `[0]` unconditionally. After the reactor refactor, `getEffluent` returns a single envelope — pre-fix settler would crash with `Cannot read properties of undefined (reading 'payload')`. The fix:
|
||||
|
||||
```js
|
||||
const raw = this.upstreamReactor.getEffluent;
|
||||
const effluent = Array.isArray(raw) ? raw[0] : raw;
|
||||
```
|
||||
|
||||
If you maintain a fork of settler from before that date, port this guard.
|
||||
|
||||
### From topic-aliased payloads
|
||||
|
||||
Both `influent` and `setInfluent` are accepted as aliases for `data.influent`. A one-time deprecation warning fires the first time each alias is seen. Tracked for removal; use the canonical `data.influent` in new flows.
|
||||
|
||||
---
|
||||
|
||||
## Related pages
|
||||
|
||||
| Page | Why |
|
||||
|:---|:---|
|
||||
| [Home](Home) | Intuitive overview |
|
||||
| [Reference — Contracts](Reference-Contracts) | Topic + config + child filters |
|
||||
| [Reference — Architecture](Reference-Architecture) | Code map, reactor ↔ settler wiring, mass-balance math |
|
||||
| [Reference — Examples](Reference-Examples) | Shipped flows + the TODO list for production-grade demos |
|
||||
| [reactor — Limitations](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Reference-Limitations) | The upstream parent — effluent shape contract |
|
||||
| [EVOLV — output-coverage rule](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/output-coverage.md) | Platform-wide output-coverage requirement |
|
||||
19
wiki/_Sidebar.md
Normal file
19
wiki/_Sidebar.md
Normal file
@@ -0,0 +1,19 @@
|
||||
### settler
|
||||
|
||||
- [Home](Home)
|
||||
|
||||
**Reference**
|
||||
|
||||
- [Contracts](Reference-Contracts)
|
||||
- [Architecture](Reference-Architecture)
|
||||
- [Examples](Reference-Examples)
|
||||
- [Limitations](Reference-Limitations)
|
||||
|
||||
**Related**
|
||||
|
||||
- [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home)
|
||||
- [reactor wiki](https://gitea.wbd-rd.nl/RnD/reactor/wiki/Home)
|
||||
- [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home)
|
||||
- [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns)
|
||||
- [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
|
||||
- [Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry)
|
||||
Reference in New Issue
Block a user