diff --git a/.gitignore b/.gitignore
index 1170717..8f5a1e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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.*
diff --git a/LICENSE b/LICENSE
index 6d8cea4..0fe7849 100644
--- a/LICENSE
+++ b/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.
diff --git a/README.md b/README.md
index 3a32a8a..5ad354f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
-# Sludge Settler
-
+# Sludge Settler
+
Sludge settler node
\ No newline at end of file
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..42ca027
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,8 @@
+# settler Example Flows
+
+Import-ready Node-RED examples for settler.
+
+## Files
+- basic.flow.json
+- integration.flow.json
+- edge.flow.json
diff --git a/examples/basic.flow.json b/examples/basic.flow.json
new file mode 100644
index 0000000..1610376
--- /dev/null
+++ b/examples/basic.flow.json
@@ -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":[]}
+]
diff --git a/examples/edge.flow.json b/examples/edge.flow.json
new file mode 100644
index 0000000..04d37b1
--- /dev/null
+++ b/examples/edge.flow.json
@@ -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":[]}
+]
diff --git a/examples/integration.flow.json b/examples/integration.flow.json
new file mode 100644
index 0000000..65c0b3a
--- /dev/null
+++ b/examples/integration.flow.json
@@ -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":[]}
+]
diff --git a/package.json b/package.json
index c2150aa..ca2e650 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"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"
},
"node-red": {
"nodes": {
diff --git a/settler.html b/settler.html
index 5ca254f..24578fb 100644
--- a/settler.html
+++ b/settler.html
@@ -1,68 +1,68 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settler.js b/settler.js
index 12fbb6b..db68c4e 100644
--- a/settler.js
+++ b/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}`);
+ }
+ });
};
\ No newline at end of file
diff --git a/src/nodeClass.js b/src/nodeClass.js
index 4757683..f0a8c11 100644
--- a/src/nodeClass.js
+++ b/src/nodeClass.js
@@ -1,114 +1,121 @@
-const { Settler } = require('./specificClass.js');
-
-
-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;
-
- this._loadConfig(uiConfig)
- this._setupClass();
-
- this._attachInputHandler();
- this._registerChild();
- this._startTickLoop();
- this._attachCloseHandler();
- }
-
- /**
- * Handle node-red input messages
- */
+const { Settler } = require('./specificClass.js');
+
+
+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;
+
+ this._loadConfig(uiConfig)
+ this._setupClass();
+
+ this._attachInputHandler();
+ this._registerChild();
+ this._startTickLoop();
+ this._attachCloseHandler();
+ }
+
+ /**
+ * 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:
- console.log("Unknown topic: " + msg.topic);
+ try {
+ switch (msg.topic) {
+ case 'registerChild': {
+ const childId = msg.payload;
+ const childObj = this.RED.nodes.getNode(childId);
+ if (!childObj || !childObj.source) {
+ this.source?.logger?.warn(`registerChild skipped: missing child/source for id=${childId}`);
+ break;
+ }
+ this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
+ break;
+ }
+ default:
+ this.source?.logger?.warn(`Unknown topic: ${msg.topic}`);
+ }
+ } catch (error) {
+ this.source?.logger?.error(`Input handler failure: ${error.message}`);
}
- if (done) {
+ if (typeof done === 'function') {
done();
}
});
}
-
- /**
- * Parse node configuration
- * @param {object} uiConfig Config set in UI in node-red
- */
- _loadConfig(uiConfig) {
- this.config = {
- general: {
- name: uiConfig.name || this.name,
- id: this.node.id,
- unit: null,
- logging: {
- enabled: uiConfig.enableLog,
- logLevel: uiConfig.logLevel
- }
- },
- functionality: {
- positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified
- softwareType: "settler" // should be set in config manager
- }
- }
- }
-
- /**
- * 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]);
- }
-
+
+ /**
+ * Parse node configuration
+ * @param {object} uiConfig Config set in UI in node-red
+ */
+ _loadConfig(uiConfig) {
+ this.config = {
+ general: {
+ name: uiConfig.name || this.name,
+ id: this.node.id,
+ unit: null,
+ logging: {
+ enabled: uiConfig.enableLog,
+ logLevel: uiConfig.logLevel
+ }
+ },
+ functionality: {
+ positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified
+ softwareType: "settler" // should be set in config manager
+ }
+ }
+ }
+
+ /**
+ * 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();
+ if (typeof done === 'function') done();
});
}
}
-
-module.exports = nodeClass;
\ No newline at end of file
+
+module.exports = nodeClass;
diff --git a/src/specificClass.js b/src/specificClass.js
index 5b87aef..3f2627c 100644
--- a/src/specificClass.js
+++ b/src/specificClass.js
@@ -1,144 +1,157 @@
-const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions');
-const EventEmitter = require('events');
-
-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
-
- 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
- }
-
- 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("atEquipment").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;
- }
-
- 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() }
- ];
- }
-
- 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}`;
-
- // 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")
- .position(position)
- .value(eventData.value, eventData.timestamp, eventData.unit);
-
- this._updateMeasurement(measurementType, eventData.value, position, eventData);
- });
- }
-
- _connectReactor(reactorChild) {
- if (reactorChild.config.functionality.positionVsParent != "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];
- this.F_in = effluent.payload.F;
- this.Cs_in = effluent.payload.C;
- });
- }
-
- _connectMachine(machineChild) {
- if (machineChild.config.functionality.positionVsParent == "downstream") {
- machineChild.upstreamSource = this;
- this.returnPump = machineChild;
- return;
- }
- this.logger.warn(`Failed to register machine child.`);
- }
-
- _updateMeasurement(measurementType, value, position, context) {
- switch(measurementType) {
- case "quantity (tss)":
- this.C_TS = value;
- break;
-
- default:
- this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
- return;
- }
- }
-}
-
-module.exports = { Settler };
\ No newline at end of file
+const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions');
+const EventEmitter = require('events');
+
+// Compatibility-safe array clone for Node runtimes without global structuredClone.
+function cloneArray(values) {
+ if (typeof structuredClone === 'function') {
+ return structuredClone(values);
+ }
+ return Array.isArray(values) ? [...values] : values;
+}
+
+/**
+ * Settler domain model.
+ * Splits influent into effluent, sludge and return sludge based on solids balance.
+ */
+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
+
+ 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
+ }
+
+ 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("atEquipment").getCurrentValue(), F_s);
+ }
+ const F_so = F_s - F_sr;
+
+ // effluent
+ const Cs_eff = cloneArray(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 = cloneArray(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;
+ }
+
+ 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() }
+ ];
+ }
+
+ 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}`;
+
+ // 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")
+ .position(position)
+ .value(eventData.value, eventData.timestamp, eventData.unit);
+
+ this._updateMeasurement(measurementType, eventData.value, position, eventData);
+ });
+ }
+
+ _connectReactor(reactorChild) {
+ if (reactorChild.config.functionality.positionVsParent != "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 raw = this.upstreamReactor.getEffluent;
+ const effluent = Array.isArray(raw) ? raw[0] : raw;
+ this.F_in = effluent.payload.F;
+ this.Cs_in = effluent.payload.C;
+ });
+ }
+
+ _connectMachine(machineChild) {
+ if (machineChild.config.functionality.positionVsParent == "downstream") {
+ machineChild.upstreamSource = this;
+ this.returnPump = machineChild;
+ return;
+ }
+ this.logger.warn(`Failed to register machine child.`);
+ }
+
+ _updateMeasurement(measurementType, value, position, context) {
+ switch(measurementType) {
+ case "quantity (tss)":
+ this.C_TS = value;
+ break;
+
+ default:
+ this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
+ return;
+ }
+ }
+}
+
+module.exports = { Settler };
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..bf56344
--- /dev/null
+++ b/test/README.md
@@ -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
diff --git a/test/basic/.gitkeep b/test/basic/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/basic/structure-module-load.basic.test.js b/test/basic/structure-module-load.basic.test.js
new file mode 100644
index 0000000..a802a4e
--- /dev/null
+++ b/test/basic/structure-module-load.basic.test.js
@@ -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');
+ });
+});
diff --git a/test/edge/.gitkeep b/test/edge/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/edge/structure-examples-node-type.edge.test.js b/test/edge/structure-examples-node-type.edge.test.js
new file mode 100644
index 0000000..6e24585
--- /dev/null
+++ b/test/edge/structure-examples-node-type.edge.test.js
@@ -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);
+});
diff --git a/test/helpers/.gitkeep b/test/helpers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/integration/.gitkeep b/test/integration/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/integration/structure-examples.integration.test.js b/test/integration/structure-examples.integration.test.js
new file mode 100644
index 0000000..d061bbc
--- /dev/null
+++ b/test/integration/structure-examples.integration.test.js
@@ -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);
+ }
+});