Dynamic Range Function

Good Morning everyone,

I was hoping to get some input or advice on how to create a dynamic range function in Node-Red. I am not too familiar with JS or Node-Red (been using it for the last couple weeks) and noticed that the Range node uses fixed values for its output.

I poked around the forums here and at node red to see what I can find. hotNipi over at NR posted a snipet (https://discourse.nodered.org/t/dynamic-input-for-range-node/3091/8) that has got me on the right track but I have been staring at my flow for too long and getting hung up. Here is what he posted (emphasis mine):

var value = msg.payload; //value to be ranged //range limits
var inputrange = msg.inputrange || [0,100];//expected range of input value. an array containing min and max
var outputrange = msg.outputrange || [0,1000] // range into the value to be converted. an array containing min and max
// range limits can also be represented as Object if you like.
// var ranges = {mininput:0,maxinput:100,minoutput:50,maxoutput:50000};
//you should store input and output ranges into context memory
//so you don’t need to send them with every message but only when needed
// see https://nodered.org/docs/writing-functions#storing-data
//brain of the whole thing. hard work is done here
const mapNumber = (number, in_min, in_max, out_min, out_max) => {
return (number - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
};
// here how the brain is used
var output = mapNumber(value,inputrange[0],inputrange[1],outputrange[0],outputrange[1]);
// hard work is done, place the ranged number into outgoing msg payload
msg.payload = output;
return msg;

The above code makes sense to me as is, but I would like the user to be able to manipulate the max output of the range. The bolded parts are what I suspect needs to be adjusted, but everything I am coming with is not functioning the way I expected it to.

Any advice or help would be much appreciated!

I think the most important change would be to use the context memory they refer to in the comments. Rather than just relying on the variables that are handed along with “msg”, using values stored in either “flow” or “global” context means you can change the values from some other node and then reference the current value in this JavaScript function.

There is a ton of information on how it works and what the specific syntax is on the linked page
https://nodered.org/docs/writing-functions#storing-data if you want to know more, but the most important thing is accessing that storage for the outputrange variable definition:

var outputrange = flow.get('outputrange') || [0,1000]

The || [0,1000] will set the output range to a default of 0-1000 if you haven’t already set this flow-context outputrange variable, otherwise outputrange[0] and outputrange[1] will be whatever you MIN and MAX you decide with flow.set('outputrange',[MIN, MAX]); or with a change node by setting the values directly:

image

Since these are just injected integers you could get these values form anywhere like a groov View node, a text file, SQL database, or anywhere you want as long as you can get the number into Node-RED.

I put together this quick sample flow for you to import and experiment with:

[{"id":"a310242b.a0c168","type":"function","z":"4bbdc5de.933b8c","name":"Dynamic Range","func":"var value = msg.payload; //value to be ranged\n//range limits:\nvar inputrange = flow.get('inputrange') || [0,100];//expected range of input value. an array containing min and max. default to 0-100\nvar outputrange = flow.get('outputrange') || [0,1000] // range into the value to be converted. an array containing min and max. default to 0-1000\n\n// brain of the whole thing. hard work is done here\nconst mapNumber = (number, in_min, in_max, out_min, out_max) => {\n    return (number - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;\n};\n// here how the brain is used\nvar output = mapNumber(value,inputrange[0],inputrange[1],outputrange[0],outputrange[1]);\n// hard work is done, place the ranged number into outgoing msg payload\nmsg.payload = output;\nreturn msg;","outputs":1,"noerr":0,"x":920,"y":660,"wires":[["24312b93.142c64"]]},{"id":"9bcaf0a2.9419f","type":"inject","z":"4bbdc5de.933b8c","name":"","topic":"","payload":"22","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":690,"y":620,"wires":[["a310242b.a0c168"]]},{"id":"465ef83.c1e4808","type":"inject","z":"4bbdc5de.933b8c","name":"","topic":"","payload":"33.333","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":690,"y":660,"wires":[["a310242b.a0c168"]]},{"id":"20580eb5.053222","type":"inject","z":"4bbdc5de.933b8c","name":"","topic":"","payload":"75","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":690,"y":700,"wires":[["a310242b.a0c168"]]},{"id":"24312b93.142c64","type":"debug","z":"4bbdc5de.933b8c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1110,"y":660,"wires":[]},{"id":"a6272bb2.2dd048","type":"change","z":"4bbdc5de.933b8c","name":"","rules":[{"t":"set","p":"inputrange","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":640,"wires":[[]]},{"id":"5b5f190d.19ae88","type":"change","z":"4bbdc5de.933b8c","name":"","rules":[{"t":"set","p":"outputrange","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":680,"wires":[[]]},{"id":"7087fde5.206a54","type":"inject","z":"4bbdc5de.933b8c","name":"","topic":"","payload":"[20,80]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":640,"wires":[["a6272bb2.2dd048"]]},{"id":"76c0cfac.a7c7","type":"inject","z":"4bbdc5de.933b8c","name":"","topic":"","payload":"[100,200]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":200,"y":680,"wires":[["5b5f190d.19ae88"]]},{"id":"699e10fe.a7ded","type":"inject","z":"4bbdc5de.933b8c","name":"","topic":"","payload":"[0,1000]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":200,"y":720,"wires":[["5b5f190d.19ae88"]]},{"id":"e06ff74c.770788","type":"inject","z":"4bbdc5de.933b8c","name":"","topic":"","payload":"[0,100]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":600,"wires":[["a6272bb2.2dd048"]]}]

Let me know if that works for you or if you have any other questions ~

Hey Torchard,

Thanks for the quick reply, Ill start playing around with your example and see what I can come up with. Hopefully I have something to post soon.

1 Like

I think I figured it out, do you mind looking at it to see if it makes sense/isnt flaky?

[{"id":"3f212665.7f3d9a","type":"tab","label":"Flow 2","disabled":false,"info":""},{"id":"7da1353d.c0389c","type":"function","z":"3f212665.7f3d9a","name":"Dynamic Range","func":"var value = msg.payload; //value to be ranged\n//range limits:\nvar inputrange = msg.inputrange || [0,10];//expected range of input value. an array containing min and max. default to 0-100\nvar outputrange = flow.get('outputrange') || [0,1000] // range into the value to be converted. an array containing min and max. default to 0-1000\n\n// brain of the whole thing. hard work is done here\nconst mapNumber = (number, in_min, in_max, out_min, out_max) => {\n    return (number - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;\n};\n// here how the brain is used\nvar output = mapNumber(value,inputrange[0],inputrange[1],outputrange[0],outputrange[1]);\n// hard work is done, place the ranged number into outgoing msg payload\nmsg.payload = output;\nreturn msg;","outputs":1,"noerr":0,"x":800,"y":300,"wires":[["8f39b044.c2441","83dbd0d6.d7e22"]]},{"id":"8f39b044.c2441","type":"debug","z":"3f212665.7f3d9a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1290,"y":300,"wires":[]},{"id":"9f274cef.126c6","type":"comment","z":"3f212665.7f3d9a","name":"MESSY EXAMPLE - Open comment to see notes","info":"","x":260,"y":60,"wires":[]},{"id":"2ad442dc.7196de","type":"ui_text_input","z":"3f212665.7f3d9a","name":"Variable Output","label":"Variable Output","tooltip":"","group":"aee2863e.bee688","order":2,"width":14,"height":1,"passthru":true,"mode":"number","delay":300,"topic":"","x":140,"y":480,"wires":[["fc717c54.a8c71"]]},{"id":"c7cbb32f.51421","type":"groov-io-read","z":"3f212665.7f3d9a","device":"480e5a19.2351e4","dataType":"channel-analog","moduleIndex":"0","channelIndex":"1","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"Speed Reference [AI]","x":440,"y":260,"wires":[["26fd3fff.bdbcf"]]},{"id":"5124dfae.d3163","type":"inject","z":"3f212665.7f3d9a","name":"","topic":"","payload":"10","payloadType":"num","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":150,"y":300,"wires":[["7da1353d.c0389c"]]},{"id":"a4426b97.25d488","type":"function","z":"3f212665.7f3d9a","name":"Put Max range output into array","func":"msg.payload = [0, (parseFloat(flow.get (\"maxrangeout\")))];\nreturn msg;\n\n\n\n//var val1 = parseInt(flow.get(\"val1\") || \"0\");\n//msg.payload = parseInt(flow.get(\"val2\") || \"0\") * val1;\n//return msg;","outputs":1,"noerr":0,"x":770,"y":480,"wires":[["6cfeca1e.04f114"]]},{"id":"fc717c54.a8c71","type":"change","z":"3f212665.7f3d9a","name":"","rules":[{"t":"set","p":"maxrangeout","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":480,"wires":[["a4426b97.25d488"]]},{"id":"6cfeca1e.04f114","type":"change","z":"3f212665.7f3d9a","name":"","rules":[{"t":"set","p":"outputrange","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1100,"y":480,"wires":[[]]},{"id":"26fd3fff.bdbcf","type":"ui_artlessgauge","z":"3f212665.7f3d9a","group":"f4b6d5b4.0bc498","order":12,"width":8,"height":1,"name":"Speed Reference","icon":"fa-bolt","label":"Speed Reference","unit":"VOLTS","layout":"linear","decimals":"2","differential":false,"minmax":false,"colorTrack":"#555555","colorFromTheme":true,"property":"payload","sectors":[{"val":0,"col":"#ee343f","t":"min","dot":0},{"val":10,"col":"#ee343f","t":"max","dot":0}],"lineWidth":"5","bgcolorFromTheme":true,"diffCenter":"0","x":450,"y":220,"wires":[]},{"id":"83dbd0d6.d7e22","type":"ui_artlessgauge","z":"3f212665.7f3d9a","group":"f4b6d5b4.0bc498","order":2,"width":4,"height":4,"name":"Converted Range","icon":"fa-tint","label":"Converted Range","unit":"TPH","layout":"radial","decimals":"2","differential":false,"minmax":false,"colorTrack":"#555555","colorFromTheme":true,"property":"payload","sectors":[{"val":0,"col":"#ee343f","t":"min","dot":0},{"val":50,"col":"#ee343f","t":"max","dot":0}],"lineWidth":3,"bgcolorFromTheme":true,"diffCenter":"","x":790,"y":260,"wires":[]},{"id":"b99f06d0.f514b8","type":"comment","z":"3f212665.7f3d9a","name":"The 'Number' portion of the conversion equation","info":"Instead of injecting a single digit, I will be using a 0-10VDC signal.","x":460,"y":180,"wires":[]},{"id":"abab23bf.5eaf1","type":"comment","z":"3f212665.7f3d9a","name":"Dynamic Range","info":"No need to adjust the Inputrange min/max as my 'Speed Reference' has a fixed upper and lower range (0-10V)","x":800,"y":340,"wires":[]},{"id":"47c4cb1e.d67ff4","type":"comment","z":"3f212665.7f3d9a","name":"Max Output ","info":"User can manipulate the Max Output range through dashboard","x":130,"y":440,"wires":[]},{"id":"5cf06fdf.66844","type":"comment","z":"3f212665.7f3d9a","name":"Max Output Number Change","info":"Takes the Max Output Range and puts into an array","x":820,"y":440,"wires":[]},{"id":"aee2863e.bee688","type":"ui_group","z":"","name":"Calibration","tab":"8e1333b9.b898d","order":4,"disp":true,"width":30,"collapse":false},{"id":"480e5a19.2351e4","type":"groov-io-device","z":"","address":"localhost","msgQueueFullBehavior":"DROP_OLD"},{"id":"f4b6d5b4.0bc498","type":"ui_group","z":"","name":"AC System","tab":"8e1333b9.b898d","order":1,"disp":true,"width":"8","collapse":false},{"id":"8e1333b9.b898d","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

@Thom I can’t see your ui_artlessgauges in my dashboard – even after installing the package – are they working OK for you? I’m curious what firmware and Node-RED version are you using.


Without being able to see the gauges, some things do still jump out to me from the flow – mainly that the Speed Reference input must wire into the dynamic range function, otherwise the only value that will ever be mapped to the new range is the initial 10 from the inject node.

If you want to see both the raw 0-10 value and the new scaled result just split the output so that each new value both goes to a gauge as well as getting mapped to the new range.
That would look something like this (note that I’m using an input node which automatically scans the channel – if you use a read node make sure that you add an inject node to trigger it on some interval):

image

The other thing is less critical, just a suggestion to put the max output value directly into outputrange and avoid creating the extra maxrangeout value:

flow.set('outputrange', [0, parseFloat(msg.payload)]);

Here’s a flow that implements those two big changes into my previous example, just using the default gauges:

image

[{"id":"3a85421b.107d1e","type":"groov-io-input","z":"8d102daf.c684b","device":"68d9925c.d6a2cc","dataType":"channel-analog","moduleIndex":"0","channelIndex":"2","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","sendInitialValue":true,"deadband":"1","scanTimeSec":"1.0","name":"","x":180,"y":360,"wires":[["4088104a.895e8","a4f8a749.f61208"]]},{"id":"4088104a.895e8","type":"ui_gauge","z":"8d102daf.c684b","name":"raw reference","group":"7a27683d.775c38","order":1,"width":6,"height":3,"gtype":"gage","title":"Raw Input","label":"Volts","format":"{{value}}","min":0,"max":10,"colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":400,"y":360,"wires":[]},{"id":"b51c3e80.10c84","type":"ui_text_input","z":"8d102daf.c684b","name":"Max Output","label":"Max Output","tooltip":"","group":"7a27683d.775c38","order":2,"width":6,"height":2,"passthru":true,"mode":"number","delay":300,"topic":"","x":190,"y":480,"wires":[["c0d115c3.2f7a58"]]},{"id":"a4f8a749.f61208","type":"function","z":"8d102daf.c684b","name":"Dynamic Range","func":"var value = msg.payload; //value to be ranged\n//range limits:\nvar inputrange = msg.inputrange || [0,10];//expected range of input value. an array containing min and max. default to 0-100\nvar outputrange = flow.get('outputrange') || [0,1000] // range into the value to be converted. an array containing min and max. default to 0-1000\n\n// brain of the whole thing. hard work is done here\nconst mapNumber = (number, in_min, in_max, out_min, out_max) => {\n    return (number - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;\n};\n// here how the brain is used\nvar output = mapNumber(value,inputrange[0],inputrange[1],outputrange[0],outputrange[1]);\n// hard work is done, place the ranged number into outgoing msg payload\nmsg.payload = output;\nreturn msg;","outputs":1,"noerr":0,"x":400,"y":400,"wires":[["662219ad.9d6f48"]]},{"id":"662219ad.9d6f48","type":"ui_gauge","z":"8d102daf.c684b","name":"converted output","group":"7a27683d.775c38","order":3,"width":6,"height":3,"gtype":"gage","title":"Converted Output","label":"units","format":"{{value}}","min":0,"max":"1000","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":610,"y":400,"wires":[]},{"id":"f5c947de.c52518","type":"debug","z":"8d102daf.c684b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":600,"y":480,"wires":[]},{"id":"c0d115c3.2f7a58","type":"function","z":"8d102daf.c684b","name":"save [min,max]","func":"flow.set('outputrange', [0, parseFloat(msg.payload)]);\nreturn msg;","outputs":1,"noerr":0,"x":400,"y":480,"wires":[["f5c947de.c52518"]]},{"id":"68d9925c.d6a2cc","type":"groov-io-device","z":"","address":"localhost","msgQueueFullBehavior":"DROP_OLD"},{"id":"7a27683d.775c38","type":"ui_group","z":"","name":"Dynamic Range","tab":"a91426fd.83adf8","order":1,"disp":true,"width":"6","collapse":false},{"id":"a91426fd.83adf8","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

As for the the Analog signal, it is wired directly to the Dynamic Range function, I just forgot to put it back in the flow before sending it to you. Like you said I have a gauge that is wired to the Analog node to read the raw signal as well.

I am running NR on a RIO and not sure how to find the firmware but here is what my Manage page says:

In regards to the output range, thank you for that, I knew there had to be a better way of doing it. Heres what it looks like now.

Thanks for all your help! This is working perfectly now, glad to have people like you at Opto, makes my headaches far less painful.

1 Like

Good to hear that everything is working, that flow looks great!

Regarding the RIO firmware version, just select Info and Help from the groov Manage main menu, then click About and you’ll see the system version at the top:


With that said, you do have the same Node-RED version that I do, so there’s no problem there, it’s just interesting that the gauges wouldn’t appear in my browser… I’ll play around with it a bit more and see if I can figure it out.


Also, if you do make any other discoveries or improvements please feel free to come back and add them to this thread for others to make use of as well!