Dashboard slider control via arrow keys on physical keyboard

I've successfully developed and deployed a Node-Red instance on a RPI3B+, and am controlling a rover via the GPIO pins. Unfortunately it is tedious to drive a rover with sliders via a browser; I'd like to 1). use the arrow keys on the keyboard or 2). use an X-Box controller. I've tried for two weeks to find a solution, and I'm hoping someone here can assist. I've tried many keyboard node offerings from the pallet, as well as the node-red-contrib-game_controllerizer suite of tools. Again, everything works just fine from the browser, I'm trying to control this device from a keyboard or controller on a distant end (i.e. the RPI is on the rover and moving around, and I'm logged into the browser on a phone or laptop, with a physical keyboard or controller).

[
    {
        "id": "280f2a0928a5e24e",
        "type": "tab",
        "label": "Flow 1",
        "disabled": false,
        "info": ""
    },
    {
        "id": "ddd82c7215917174",
        "type": "rpi-gpio out",
        "z": "280f2a0928a5e24e",
        "name": "Steering",
        "pin": "32",
        "set": "",
        "level": "0",
        "freq": "100",
        "out": "pwm",
        "x": 560,
        "y": 200,
        "wires": []
    },
    {
        "id": "3bf56581b14be4bd",
        "type": "ui_slider",
        "z": "280f2a0928a5e24e",
        "name": "Steering",
        "label": "Turn",
        "tooltip": "",
        "group": "f1774614ab0650b3",
        "order": 3,
        "width": 5,
        "height": 1,
        "passthru": true,
        "outs": "all",
        "topic": "topic",
        "topicType": "msg",
        "min": "16",
        "max": "24",
        "step": ".05",
        "x": 360,
        "y": 200,
        "wires": [
            [
                "ddd82c7215917174"
            ]
        ]
    },
    {
        "id": "fcca296080d599af",
        "type": "rpi-gpio out",
        "z": "280f2a0928a5e24e",
        "name": "Throttle",
        "pin": "36",
        "set": false,
        "level": "0",
        "freq": "97",
        "out": "pwm",
        "x": 1040,
        "y": 260,
        "wires": []
    },
    {
        "id": "4ce6d990f76a7fba",
        "type": "ui_slider",
        "z": "280f2a0928a5e24e",
        "name": "Throttle Forward",
        "label": "Forward",
        "tooltip": "",
        "group": "f1774614ab0650b3",
        "order": 1,
        "width": 1,
        "height": 5,
        "passthru": true,
        "outs": "all",
        "topic": "topic",
        "topicType": "msg",
        "min": "14.7",
        "max": "18",
        "step": ".05",
        "x": 860,
        "y": 200,
        "wires": [
            [
                "fcca296080d599af",
                "e881cdfe78376e37"
            ]
        ],
        "icon": "font-awesome/fa-arrows-h"
    },
    {
        "id": "760d0e535d0fb0a5",
        "type": "ui_slider",
        "z": "280f2a0928a5e24e",
        "name": "Throttle Reverse",
        "label": "Reverse",
        "tooltip": "",
        "group": "f1774614ab0650b3",
        "order": 14,
        "width": 1,
        "height": 5,
        "passthru": true,
        "outs": "all",
        "topic": "topic",
        "topicType": "msg",
        "min": "14.5",
        "max": "10",
        "step": ".05",
        "x": 860,
        "y": 320,
        "wires": [
            [
                "fcca296080d599af",
                "88a0f188b92e1ffb"
            ]
        ]
    },
    {
        "id": "88a0f188b92e1ffb",
        "type": "ui_gauge",
        "z": "280f2a0928a5e24e",
        "name": "Reverse Power",
        "group": "f1774614ab0650b3",
        "order": 8,
        "width": 2,
        "height": 2,
        "gtype": "gage",
        "title": "Reverse",
        "label": "units",
        "format": "{{value}}",
        "min": "14.5",
        "max": "10",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "x": 1220,
        "y": 400,
        "wires": []
    },
    {
        "id": "e881cdfe78376e37",
        "type": "ui_gauge",
        "z": "280f2a0928a5e24e",
        "name": "Forward Power",
        "group": "f1774614ab0650b3",
        "order": 7,
        "width": 2,
        "height": 2,
        "gtype": "gage",
        "title": "Forward",
        "label": "units",
        "format": "{{value}}",
        "min": "14.7",
        "max": "18",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "x": 1220,
        "y": 120,
        "wires": []
    },
    {
        "id": "f1774614ab0650b3",
        "type": "ui_group",
        "name": "Truck",
        "tab": "7dfab6ac996b6b2a",
        "order": 1,
        "disp": true,
        "width": 24,
        "collapse": false
    },
    {
        "id": "7dfab6ac996b6b2a",
        "type": "ui_tab",
        "name": "Control",
        "icon": "dashboard",
        "disabled": false,
        "hidden": false
    }
]

I think that's more of a phone / controller problem, no? Node-RED doesn't "care" whether you access through a browser on a phone or a browser on a PC.

And welcome to the forum btw :wink:

1 Like

If you are on a remote browser viewing dashboard then you could simply use onkeyup against body in a UI Template node that sends msgs to node-red.

m8rApPaxWc

[{"id":"d65b91c9dd547fab","type":"ui_template","z":"e78eac1c.8b9cf","group":"a348b0ef9d248038","name":"","order":7,"width":0,"height":0,"format":"<div id=\"key_press\">waiting key</div>\n<script type=\"text/javascript\">\n(function(scope) {\n  const _scope = scope;\n  $('body').on(\"keyup\", function(e) { \n      var keyCode = (e || event || {}).keyCode;\n      if(keyCode >= 37 && keyCode <= 40) {\n        $(\"#key_press\").text(\"Detected: \" + keyCode);\n        _scope.send({payload: keyCode});\n      } \n  });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":3480,"y":160,"wires":[["96735c606d0b6648"]]},{"id":"78be99a6182b7071","type":"debug","z":"e78eac1c.8b9cf","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":3750,"y":160,"wires":[]},{"id":"96735c606d0b6648","type":"rbe","z":"e78eac1c.8b9cf","name":"","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":3610,"y":160,"wires":[["78be99a6182b7071"]]},{"id":"a348b0ef9d248038","type":"ui_group","name":"Default","tab":"3316deba5234ba30","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"3316deba5234ba30","type":"ui_tab","name":"test","icon":"dashboard","disabled":false,"hidden":false}]

Use ui_buttons instead? Very similar to keys on the keyboard, just tappling with a mouse, or finger on the phone.

There is also a joystick node available.

As for a controller... I found this when searching in the palette. (EDIT.. I see you already found it... so, what's wrong with it?)

And on the note of using the palette... have you searched for keyboard? I found a few nodes that might do what you seek.

Appreciate the idea -- I've tried a bunch, the issue lies (I believe) in that either the tools are made to output keystrokes, or to do something else niche that isn't tied to a physical keyboard keystroke. I could be wrong, but no, those were my first effort TBH. Thanks though.

Steve,

I think your concept has the solution, but my problem with implementation may be a shortcoming of my ability to string the nodes together logically. So I can force a keystroke to manifest on the screen (sweet -- THANKS!). But how do I then translate that into a slider action. The slider produces a range of values, which (as per the shared node example above) is from 16-24 in steps of .5.

So perhaps your solution could be tied to a one up or one down (in .5 values) starting at 16 (lowest) and incrementally with each keypress increase to 24? I think if that were the case I would tie your solution (series of nodes) directly into the RPI PWM controller (GPIO) node, perhaps parallel to the slider. ..so rather than control the slider replace it with the physical "plan b"?

Can you describe how such a script would look (I'm good for the GUI portion, but not a dev by trade). Thanks again Sir, solid progress!

I don't know if you already figured this out, but here is my interpretation.

It is not perfect... as in order for the keys to work, one must first use the slider, then either one works perfectly. I have no idea why?? I have tried many feedback loops and so on, but it is perhaps better left to someone else to point out the errors of my ways :stuck_out_tongue:

image

[{"id":"cae41fc9a5d62229","type":"ui_template","z":"534ee29537236d51","group":"a348b0ef9d248038","name":"","order":1,"width":0,"height":0,"format":"<div id=\"key_press\">Press L/R Arrow Keys to adjust Slider</div>\n<script type=\"text/javascript\">\n(function(scope) {\n  const _scope = scope;\n  $('body').on(\"keydown\", function(e) { \n            var keyCode = (e || event || {}).keyCode;\n      if(keyCode === 37) {\n        _scope.send({key: \"left\"});\n      } else if(keyCode === 40) {\n        _scope.send({key: \"right\"});\n      }\n  });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":320,"y":240,"wires":[["048e2c6d21b6aadc"]]},{"id":"15a523cd0d09acc2","type":"inject","z":"534ee29537236d51","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"20.5","payloadType":"num","x":470,"y":180,"wires":[["7d7d241cd8780f1d","468cf0e4c3c9e976"]]},{"id":"048e2c6d21b6aadc","type":"rbe","z":"534ee29537236d51","name":"","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":470,"y":240,"wires":[["7d7d241cd8780f1d"]]},{"id":"7d7d241cd8780f1d","type":"function","z":"534ee29537236d51","name":"","func":"let keyVal = msg.key;\nlet slideVal = msg.payload;\nif (keyVal == \"left\") {\n    msg.slide = slideVal - .5;\n}  else if (keyVal == \"right\") {\n    slideVal = slideVal + .5;\n} else {\n    slideVal = slideVal;\n}\nmsg.payload = slideVal;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":240,"wires":[["a19baf3870091b82","c7529cb062bd6a65"]]},{"id":"a19baf3870091b82","type":"ui_slider","z":"534ee29537236d51","name":"","label":"slider","tooltip":"","group":"a348b0ef9d248038","order":2,"width":0,"height":0,"passthru":true,"outs":"end","topic":"","topicType":"str","min":"16","max":"25","step":".5","x":770,"y":240,"wires":[["ead55e52592c4dfd","ef28f6f05e974917","507207b3cf23c99a"]]},{"id":"ef28f6f05e974917","type":"ui_gauge","z":"534ee29537236d51","name":"","group":"a348b0ef9d248038","order":3,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":"16","max":"25","colors":["#000080","#000080","#000080"],"seg1":"","seg2":"","x":930,"y":240,"wires":[]},{"id":"a348b0ef9d248038","type":"ui_group","name":"","tab":"7bf230132c5213a9","order":18,"disp":true,"width":"6","collapse":false},{"id":"7bf230132c5213a9","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

9PsJTX0TlD

Demo flow...

[{"id":"d65b91c9dd547fab","type":"ui_template","z":"e78eac1c.8b9cf","group":"a348b0ef9d248038","name":"","order":7,"width":0,"height":0,"format":"<div id=\"key_press\">waiting key</div>\n<script type=\"text/javascript\">\n(function(scope) {\n  const _scope = scope;\n  $('body').on(\"keyup\", function(e) { \n      var keyCode = (e || event || {}).keyCode;\n      if(keyCode >= 37 && keyCode <= 40) {\n        $(\"#key_press\").text(\"Detected Key: \" + keyCode);\n        _scope.send({payload: keyCode});\n      } \n  });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1660,"y":160,"wires":[["e14a22ea0530cff0"]]},{"id":"78be99a6182b7071","type":"debug","z":"e78eac1c.8b9cf","name":"to PMW","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":2260,"y":220,"wires":[]},{"id":"ca2131e3bc9252ca","type":"ui_slider","z":"e78eac1c.8b9cf","name":"","label":"slider","tooltip":"","group":"a348b0ef9d248038","order":1,"width":0,"height":0,"passthru":false,"outs":"all","topic":"topic","topicType":"msg","min":"16","max":"24","step":"0.5","className":"","x":1850,"y":280,"wires":[["739ea32e3e567e19"]]},{"id":"739ea32e3e567e19","type":"change","z":"e78eac1c.8b9cf","name":"","rules":[{"t":"set","p":"slider","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2020,"y":280,"wires":[["78be99a6182b7071"]]},{"id":"e14a22ea0530cff0","type":"change","z":"e78eac1c.8b9cf","name":"get slider value","rules":[{"t":"set","p":"slider","pt":"msg","to":"slider","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":1840,"y":160,"wires":[["f78440de5e7b323e"]]},{"id":"f78440de5e7b323e","type":"function","z":"e78eac1c.8b9cf","name":"inc/dec/dheck","func":"const MIN = 16;\nconst MAX = 24;\nconst INC = 0.5;\n\nlet slider = msg.slider || MIN;\n\nswitch(msg.payload) {\n    case 37:\n        //left arrow\n        slider -= INC;\n    break;\n    \n    case 39:\n        //right arrow\n        slider += INC;\n    break;\n}\n\nif (slider < MIN) {\n    slider = MIN;\n} else if (slider > MAX) {\n    slider = MAX;\n}\n\nmsg.payload = slider;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2020,"y":160,"wires":[["c93e8a8710414bed","78be99a6182b7071"]]},{"id":"c93e8a8710414bed","type":"change","z":"e78eac1c.8b9cf","name":"","rules":[{"t":"set","p":"slider","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1680,"y":280,"wires":[["ca2131e3bc9252ca"]]},{"id":"a348b0ef9d248038","type":"ui_group","name":"Default","tab":"3316deba5234ba30","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"3316deba5234ba30","type":"ui_tab","name":"test","icon":"dashboard","disabled":false,"hidden":false}]

So I spent some time experimenting. It appears that Node Red sliders AUTOMATICALLY work with the arrow keys -- first you have to put them in focus, then the up/right count up, and the left/down count down. This is useful, and was not previously noted, so if I remove the slider, how do I use the above concept to "replace" the slide so I can assign a specific key to "up" and a specific key to "down" (as in incremental PWM increases based on the up or down key). It may be easier to do without the slider complication -- I'll keep experimenting, but I'm not java fluent. Thanks again.

That's ok, this is JavaScript :wink:

Ps, the slider also moves with up and down keys if focused.

Also, if you make the dashboard size thin& tall it will be drawn vertically.

The issue is you must have it focused otherwise it doesn't work.

A different approach might be an option... Use the code I wrote to focus the slider if not focused.

Darnit!! that previously unknown (to me) fact has been plaguing me when I try to code in various means of using the keys :woozy_face: And here I thought my code was working... sometimes... but not always the way I thought it should...

Stupid, automatic, code free, works when focused, browser function :crazy_face:

EDIT, but now that I know... I was able to finish off my project.