2025-06-25 17:26:13 +02:00
/ * *
2025-07-01 15:25:07 +02:00
* node class . js
2025-06-25 17:26:13 +02:00
*
* Encapsulates all node logic in a reusable class . In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use .
* This allows us to keep the Node - RED node clean and focused on wiring up the UI and event handlers .
* /
const { outputUtils , configManager } = require ( 'generalFunctions' ) ;
const Specific = require ( "./specificClass" ) ;
class nodeClass {
/ * *
* Create a Node .
* @ param { object } uiConfig - Node - RED node configuration .
* @ param { object } RED - Node - RED runtime API .
* /
constructor ( uiConfig , RED , nodeInstance , nameOfNode ) {
// Preserve RED reference for HTTP endpoints if needed
this . node = nodeInstance ; // This is the Node-RED node instance, we can use this to send messages and update status
this . RED = RED ; // This is the Node-RED runtime API, we can use this to create endpoints if needed
this . name = nameOfNode ; // This is the name of the node, it should match the file name and the node type in Node-RED
this . source = null ; // Will hold the specific class instance
2025-07-02 17:07:19 +02:00
this . config = null ; // Will hold the merged configuration
2025-06-25 17:26:13 +02:00
// Load default & UI config
this . _loadConfig ( uiConfig , this . node ) ;
// Instantiate core Measurement class
2025-07-02 17:07:19 +02:00
this . _setupSpecificClass ( uiConfig ) ;
2025-06-25 17:26:13 +02:00
// Wire up event and lifecycle handlers
this . _bindEvents ( ) ;
this . _registerChild ( ) ;
this . _startTickLoop ( ) ;
this . _attachInputHandler ( ) ;
this . _attachCloseHandler ( ) ;
}
/ * *
* Load and merge default config with user - defined settings .
* @ param { object } uiConfig - Raw config from Node - RED UI .
* /
_loadConfig ( uiConfig , node ) {
// Merge UI config over defaults
this . config = {
general : {
name : uiConfig . name ,
id : node . id , // node.id is for the child registration process
unit : uiConfig . unit , // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
logging : {
enabled : uiConfig . enableLog ,
logLevel : uiConfig . logLevel
}
} ,
asset : {
uuid : uiConfig . assetUuid , //need to add this later to the asset model
tagCode : uiConfig . assetTagCode , //need to add this later to the asset model
supplier : uiConfig . supplier ,
category : uiConfig . category , //add later to define as the software type
type : uiConfig . assetType ,
model : uiConfig . model ,
unit : uiConfig . unit
} ,
functionality : {
positionVsParent : uiConfig . positionVsParent || 'atEquipment' , // Default to 'atEquipment' if not specified
}
} ;
// Utility for formatting outputs
this . _output = new outputUtils ( ) ;
}
/ * *
* Instantiate the core Measurement logic and store as source .
* /
2025-07-02 17:07:19 +02:00
_setupSpecificClass ( uiConfig ) {
2025-07-01 15:25:07 +02:00
const machineConfig = this . config ;
2025-06-25 17:26:13 +02:00
// need extra state for this
const stateConfig = {
general : {
logging : {
2025-07-01 15:25:07 +02:00
enabled : machineConfig . eneableLog ,
logLevel : machineConfig . logLevel
2025-06-25 17:26:13 +02:00
}
} ,
movement : {
2025-07-02 17:07:19 +02:00
speed : Number ( uiConfig . speed )
2025-06-25 17:26:13 +02:00
} ,
time : {
2025-07-02 17:07:19 +02:00
starting : Number ( uiConfig . startup ) ,
warmingup : Number ( uiConfig . warmup ) ,
stopping : Number ( uiConfig . shutdown ) ,
coolingdown : Number ( uiConfig . cooldown )
2025-06-25 17:26:13 +02:00
}
} ;
2025-07-01 15:25:07 +02:00
this . source = new Specific ( machineConfig , stateConfig ) ;
//store in node
this . node . source = this . source ; // Store the source in the node instance for easy access
2025-06-25 17:26:13 +02:00
}
/ * *
2025-07-01 17:02:51 +02:00
* Bind events to Node - RED status updates . Using internal emitter . -- > REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES
2025-06-25 17:26:13 +02:00
* /
_bindEvents ( ) {
this . source . emitter . on ( 'mAbs' , ( val ) => {
this . node . status ( { fill : 'green' , shape : 'dot' , text : ` ${ val } ${ this . config . general . unit } ` } ) ;
} ) ;
}
_updateNodeStatus ( ) {
const m = this . source ;
try {
const mode = m . currentMode ;
const state = m . state . getCurrentState ( ) ;
const flow = Math . round ( m . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( 'downstream' ) . getCurrentValue ( ) ) ;
const power = Math . round ( m . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'upstream' ) . getCurrentValue ( ) ) ;
let symbolState ;
switch ( state ) {
case "off" :
symbolState = "⬛" ;
break ;
case "idle" :
symbolState = "⏸️" ;
break ;
case "operational" :
symbolState = "⏵️" ;
break ;
case "starting" :
symbolState = "⏯️" ;
break ;
case "warmingup" :
symbolState = "🔄" ;
break ;
case "accelerating" :
symbolState = "⏩" ;
break ;
case "stopping" :
symbolState = "⏹️" ;
break ;
case "coolingdown" :
symbolState = "❄️" ;
break ;
case "decelerating" :
symbolState = "⏪" ;
break ;
}
const position = m . state . getCurrentPosition ( ) ;
const roundedPosition = Math . round ( position * 100 ) / 100 ;
let status ;
switch ( state ) {
case "off" :
status = { fill : "red" , shape : "dot" , text : ` ${ mode } : OFF ` } ;
break ;
case "idle" :
status = { fill : "blue" , shape : "dot" , text : ` ${ mode } : ${ symbolState } ` } ;
break ;
case "operational" :
status = { fill : "green" , shape : "dot" , text : ` ${ mode } : ${ symbolState } | ${ roundedPosition } % | 💨 ${ flow } m³/h | ⚡ ${ power } kW ` } ;
break ;
case "starting" :
status = { fill : "yellow" , shape : "dot" , text : ` ${ mode } : ${ symbolState } ` } ;
break ;
case "warmingup" :
status = { fill : "green" , shape : "dot" , text : ` ${ mode } : ${ symbolState } | ${ roundedPosition } % | 💨 ${ flow } m³/h | ⚡ ${ power } kW ` } ;
break ;
case "accelerating" :
status = { fill : "yellow" , shape : "dot" , text : ` ${ mode } : ${ symbolState } | ${ roundedPosition } %| 💨 ${ flow } m³/h | ⚡ ${ power } kW ` } ;
break ;
case "stopping" :
status = { fill : "yellow" , shape : "dot" , text : ` ${ mode } : ${ symbolState } ` } ;
break ;
case "coolingdown" :
status = { fill : "yellow" , shape : "dot" , text : ` ${ mode } : ${ symbolState } ` } ;
break ;
case "decelerating" :
status = { fill : "yellow" , shape : "dot" , text : ` ${ mode } : ${ symbolState } - ${ roundedPosition } % | 💨 ${ flow } m³/h | ⚡ ${ power } kW ` } ;
break ;
default :
status = { fill : "grey" , shape : "dot" , text : ` ${ mode } : ${ symbolState } ` } ;
}
return status ;
} catch ( error ) {
node . error ( "Error in updateNodeStatus: " + error . message ) ;
return { fill : "red" , shape : "ring" , text : "Status Error" } ;
}
}
/ * *
* 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 . config . general . id , positionVsParent : this . config ? . functionality ? . positionVsParent || 'atEquipment' } ,
] ) ;
} , 100 ) ;
}
/ * *
* Start the periodic tick loop to drive the Measurement class .
* /
_startTickLoop ( ) {
setTimeout ( ( ) => {
this . _tickInterval = setInterval ( ( ) => this . _tick ( ) , 1000 ) ;
// Update node status on nodered screen every second ( this is not the best way to do this, but it works for now)
this . _statusInterval = setInterval ( ( ) => {
const status = this . _updateNodeStatus ( ) ;
this . node . status ( status ) ;
} , 1000 ) ;
} , 1000 ) ;
}
/ * *
* Execute a single tick : update measurement , format and send outputs .
* /
_tick ( ) {
2025-07-01 15:25:07 +02:00
//this.source.tick();
2025-06-25 17:26:13 +02:00
const raw = this . source . getOutput ( ) ;
const processMsg = this . _output . formatMsg ( raw , this . config , 'process' ) ;
const influxMsg = this . _output . formatMsg ( raw , this . config , 'influxdb' ) ;
// Send only updated outputs on ports 0 & 1
this . node . send ( [ processMsg , influxMsg ] ) ;
}
/ * *
* Attach the node ' s input handler , routing control messages to the class .
* /
_attachInputHandler ( ) {
this . node . on ( 'input' , ( msg , send , done ) => {
/* Update to complete event based node by putting the tick function after an input event */
const m = this . source ;
switch ( msg . topic ) {
case 'registerChild' :
2025-07-01 15:25:07 +02:00
// Register this node as a child of the parent node
2025-06-25 17:26:13 +02:00
const childId = msg . payload ;
const childObj = this . RED . nodes . getNode ( childId ) ;
m . childRegistrationUtils . registerChild ( childObj . source , msg . positionVsParent ) ;
break ;
case 'setMode' :
m . setMode ( msg . payload ) ;
break ;
case 'execSequence' :
const { source , action , parameter } = msg . payload ;
m . handleInput ( source , action , parameter ) ;
break ;
case 'execMovement' :
const { source : mvSource , action : mvAction , setpoint } = msg . payload ;
m . handleInput ( mvSource , mvAction , Number ( setpoint ) ) ;
break ;
case 'flowMovement' :
const { source : fmSource , action : fmAction , setpoint : fmSetpoint } = msg . payload ;
m . handleInput ( fmSource , fmAction , Number ( fmSetpoint ) ) ;
break ;
case 'emergencystop' :
const { source : esSource , action : esAction } = msg . payload ;
m . handleInput ( esSource , esAction ) ;
break ;
case 'showCompleteCurve' :
m . showCompleteCurve ( ) ;
send ( { topic : "Showing curve" , payload : m . showCompleteCurve ( ) } ) ;
break ;
case 'CoG' :
m . showCoG ( ) ;
send ( { topic : "Showing CoG" , payload : m . showCoG ( ) } ) ;
break ;
}
} ) ;
}
/ * *
* Clean up timers and intervals when Node - RED stops the node .
* /
_attachCloseHandler ( ) {
this . node . on ( 'close' , ( done ) => {
clearInterval ( this . _tickInterval ) ;
clearInterval ( this . _statusInterval ) ;
done ( ) ;
} ) ;
}
}
module . exports = nodeClass ;