Node-RED hangs after a few deploys (external library)

We are using an external library called node-hid-stream to get data from a barcode reader. The library was installed with npm and "imported" via settings.js into Node-RED:

functionGlobalContext: {
        nodehidstream:require('node-hid-stream')

    },

To catch data from the scanner we have created a listener inside a function node. This is probably not recommended but we have not found another/better solution so far. The function node "fires" whenever data from the scanner is coming in. The function node looks like this:

try{
    //device = new hid.HID(0x05e0, 0x1200);
    msg.payload = "connected";
    msg.color = "green";
    var KeyboardLines = global.get("nodehidstream").KeyboardLines; 
    flow.set("keyboard_lines", KeyboardLines);
    
    var device = new KeyboardLines({ vendorId: 0x05e0, productId: 0x1200 }); 
    flow.set("scanner_device", device); 
    
    // this listener handles data from barcode and is fired everytime a code is scanned
    device.on("data", function(data) {
      node.warn("from scanner node (data): "+data);
      msg.payload = data;
      node.send([null,msg]);
    });

    flow.set("scanner", true);
}catch(e){
    node.warn(e);
    flow.set("scanner", false);
    msg.color = "red";
    msg.payload = "not connected";
}

return [msg, null];

In the beginning we did not add any code inside the closing tab and therefore run into problems, because a new listener was registered every time we deployed our flow. The closing tab looks as follows:

flow.get("scanner_device").close();
flow.set("scanner_device", undefined);
flow.set("keyboard_lines", undefined);
flow.set("scanner", false);

After a few deploys the Node-RED service hangs and neither the flow nor the gui (dashboard) is accessible anymore and Node-RED has to be restarted manually. We are quite sure that the problem is cause either by our function node or the external library node-hid-stream but are unable to solve the problem.

After stopping the Node-RED service the above mentioned close() command causes a timeout error, something like "error while closing the node: Close timed out"

We are using Node-RED 1.2.9
node-hid: 2.1.1
node-hid-stream: 1.1.0

Do you need to do this in a function node? Can't you create the listener in settings.js (or better still, in a module that you require into settings.js?

Ideally, you would wrap this in a simple custom node (I assume you've checked that one doesn't already exist?). So that you ended up with a hid-in node that would fire a msg every time your device.on function was triggered.


Having said that, I do have some similar code that I use in function nodes to wrap my node-drayton-wiser node.js module.

Here is an example. Note that I make sure that my events are deleted whenever this function node is executed and before setting up the event listener again.

const wiser = global.get('wiser') // Module: node-drayton-wiser

// Reset any existing event listeners
const eventNames = ['wiserChange',]
// Get rid of only the event names listed
eventNames.forEach( evtName => {
    wiser.eventEmitter.listeners(evtName).forEach( listener => {
        wiser.eventaEmitter.removeListener(evtName, listener)
    })
})

wiser.eventEmitter.on('wiserChange', function(changes) {
    msg.topic = `wiser/event/change/${changes.type}/${changes.id}`
    msg.payload = changes
    node.send(msg)
    
    // Process different types of change - output more useful MQTT events
    
    // Device-specific changes
    if ( changes.type === 'Device' ) {
        republishDeviceChanges(changes)
    }

    // Room (and device in room) & other changes
    let roomTopic = `wiser/environment/${changes.room}`
    if ( changes.changes.MeasuredTemperature ) {
        republishChanges(changes, 'MeasuredTemperature', `${roomTopic}/temperature_measured`)
    }
    if ( changes.changes.MeasuredHumidity ) {
        republishChanges(changes, 'MeasuredHumidity', `${roomTopic}/humidity`)
    }
    if ( changes.changes.CalculatedTemperature ) {
        republishChanges(changes, 'CalculatedTemperature', `${roomTopic}/temperature_calculated`)
    }
    if ( changes.changes.PercentageDemand ) {
        republishChanges(changes, 'PercentageDemand', `${roomTopic}/demand`)
    }
    if ( changes.changes.CurrentSetPoint ) {
        republishChanges(changes, 'CurrentSetPoint', `${roomTopic}/setpoint_current`)
    }
    if ( changes.changes.ScheduledSetPoint ) {
        republishChanges(changes, 'ScheduledSetPoint', `${roomTopic}/setpoint_scheduled`)
    }
    if ( changes.changes.DemandOnOffOutput ) {
        republishChanges(changes, 'DemandOnOffOutput', `${roomTopic}/demand`)
    }
    if ( changes.changes.HeatingRelayState ) {
        republishChanges(changes, 'HeatingRelayState', `${roomTopic}/relay`)
    }
    if ( changes.changes.SetpointOrigin ) {
        republishChanges(changes, 'SetpointOrigin', `${roomTopic}/setpoint_origin`)
    }

})

return [null,msg]

// ============ Utility Functions ============= //

function republishDeviceChanges(changes) {
    
    if ( changes.changes.DisplayedSignalStrength ) {
        republishChanges(changes, 'DisplayedSignalStrength', `wiser/signals/${changes.room}/${changes.id}`)
    }
    if ( changes.changes.BatteryLevel ) {
        republishChanges(changes, 'BatteryLevel', `wiser/battery/${changes.room}/${changes.id}/BatteryLevel`)
    }
    if ( changes.changes.BatteryVoltage ) {
        republishChanges(changes, 'BatteryVoltage', `wiser/battery/${changes.room}/${changes.id}/BatteryVoltage`)
    }

} // --- end of function republishDeviceChanges --- //

function republishChanges(changes, measureName='', myTopic='wiser/republish') {

    // These changes don't have a defined room because they come from the controller
    if ( changes.type === 'HeatingChannel' || changes.type === 'System' ) changes.room = 'Controller'
    
    // Output real temperatures in °C
    let mname = measureName.toLowerCase()
    let out = changes.changes[measureName]
    if ( mname.includes('temperature') || mname.includes('setpoint') || mname.includes('voltage') ) 
        out = changes.changes[measureName] / 10
    
    
    node.send({
        topic: myTopic,
        payload: out
    })
    publishUpdate(myTopic)
    
} // --- end of function republishEnvironmentChanges --- //

function publishUpdate(topicRoot) {

    node.send({
        topic: `${topicRoot}/updated`,
        payload: (new Date()).toISOString()
    })
    node.send({
        topic: `${topicRoot}/updated_by`,
        payload: 'nrmain/Heating/change-listener/publishUpdate'
    })
    
} // --- end of function publishUpdate --- //

When I get time, this will end up in a custom node.

The wiser object is instantiated in settings.js and it has a built-in event emitter object as you can see.

// ...
functionGlobalContext: {
    // ...
    wiser: require('node-drayton-wiser')(),
    // ...
},
// ...

Thanks a lot for the immediate reply @TotallyInformation. We will try to remove the listener as suggested.

I think it's better to do this inside the function node, instead of the "closing tab" (since we can't access the node object from there and therefore it is impossible to do things like node.warn("bla")). We also plan to create a custom node for this in the future. However, we believe that it would make things more complicated at the moment.

If this will solve the problem and the timeout error is unclear at the moment. We might also have to move the pointer to the device/object to a persistent context, or else it won't be accessible after a deploy. Is this why you have suggested to do things in settings.js? I didn't know this is possible.

Side note: The node-hid-stream library has some important features related to barcodes. And so far we were unable to find an existing Node-RED component which could do what we need.

I've not yet managed to find a situation where using the opening and closing tabs are useful for me. But you should certainly remove the existing listener before redoing it otherwise you will end up with multiple listeners trying to do the same thing. Eventually that may well cause a stack overflow or other out of memory issue.

Using the globals object in settings.js is the same as having a persistent context since every time Node-RED starts, the object will be created. It is also safer doing it there because some of the context handlers have to (un)serialise data and you can't do that with code in the object.

It also means that you will only ever have one device object and 1 event handler object.

Not a problem, you would be surprised (or maybe not) how many people don't check before launching in to create a new node.

Short update, problem is still not solved but I managed to find out which "line" is causing the problem:

var device = new KeyboardLines({ vendorId: 0x05e0, productId: 0x1200 });

I thought by calling device.close() and deleting all references the GC would kick in. Even tried to use node-red-contrib-gc-trigger, without success. Game over after three deploys. At least close() removes the listener. And I was able to use node.warn("...") in the closing tab by accessing node through global.get("..").

It does not seem to be a Node-RED problem but I am unable to reach the developer of node-hid-stream - quite disappointing.

As I say, I think you need to do that once in settings.js. Then you can simply pass the device as a reference, no need to keep recreating it.

Thanks again @TotallyInformation, but having code in the settings.js does not seem right to me. Also, I was unable to find Node-RED documentation regarding this.

However, I moved the device object from "flow context" to "global context" and now everything seems to work (again).

  • Create device object and store it in the global context once on start-up, deploy, re-deploy etc. global.set("scanner_device", device);

  • Use the closing tab to close the device and set the global variable to “undefined”
    global.get("scanner_device").close(); global.set("scanner_device", undefined);

settings.js is your entry point to Node-RED. It's whole purpose is to set up the configuration and load any extra data. To keep it clean, put your code into an external module - that's to say, a JavaScript file that contains a module.exports statement with the objects/variables that you want available in Node-RED contained within it. Then you can simple require it into a global variable in the same way you might load other modules you need in your flows (like the node.js core fs module for example).

The whole purpose of that capability in the settings.js file is to provide you with that flexibility. It is a very powerful tool. No point in ignoring it. It will save you having to mess with things like the close tab in function nodes and will ensure that you only ever have 1 copy loaded into Node-RED.

This is leveraging the power of node.js that Node-RED is built on.

The other advantage, is that nobody can mess it up from a flow (whether that is you or anyone else). For example, accidentally making a change that breaks the load or accidentally setting the default context store to the filing system which will break your code because it won't let you serialise your function object. If you are anything like me, you will have forgotten all this in a year's time when you next come to update your flow :grimacing: