diff --git a/README.md b/README.md index 3f05c77..c11c0d0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # balena-node-red -A Node-RED application with [balena-supervisor](https://balena.io/docs/reference/supervisor/supervisor-api/) flow [support](https://github.com/balena-io-projects/node-red-contrib-balena), can be managed remotely via balena [publicURL](https://balena.io/docs/learn/manage/actions/#enable-public-device-url) +This application consists of 2 services: +* `node-red` = A [Node-RED](https://nodered.org/) application that can be accessed remotely via the balena [publicURL](https://balena.io/docs/learn/manage/actions/#enable-public-device-url). It consists of the following 2 flows (or tabs): + * `Supervisor`: uses [node-red-contrib-balena](https://github.com/balena-io-projects/node-red-contrib-balena) to interact with the [balena-supervisor](https://balena.io/docs/reference/supervisor/supervisor-api/). + * `GPIO`: simple flow shows how the node [node-red-node-pigpiod](https://flows.nodered.org/node/node-red-node-pi-gpiod) can be used to read GPIO pins from the raspberry pi. +* `pigpiod` = this service runs the daemon [pigpiod](http://abyz.me.uk/rpi/pigpio/pigpiod.html). This daemon must be running if you want to interact with the GPIO pins of your raspberry pi using the Node-RED node [node-red-node-pigpiod](https://flows.nodered.org/node/node-red-node-pi-gpiod) -## Configure via [environment variables](https://balena.io/docs/learn/manage/serv-vars/) +## Configure node-red service via [environment variables](https://balena.io/docs/learn/manage/serv-vars/) Variable Name | Default | Description ------------ | ------------- | ------------- -PORT | `80` | the port that exposes the Node-RED UI +PORT | `1880` | the port that exposes the Node-RED editor. Note that host port 80 is mapped to 1880 so that Node-RED editor can also be accessed through the Balena publicURL if activated. USERNAME | `none` | the Node-RED admin username PASSWORD | `none` | the Node-RED admin password [hash](https://nodered.org/docs/security#generating-the-password-hash) diff --git a/docker-compose.yml b/docker-compose.yml index 41c0fb3..269fac7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,30 @@ version: '2' volumes: - resin-data: + node-red-data: services: node-red: build: ./node-red volumes: - - 'resin-data:/data' + - 'node-red-data:/data' restart: always privileged: true - network_mode: host labels: io.balena.features.supervisor-api: '1' + ports: + - 80:1880 # will make that the balena public URL maps to the node-red editor. + pigpiod: + build: ./pigpiod + restart: always + privileged: true cap_add: - - SYS_RAWIO + - SYS_RAWIO # not sure if this is really needed devices: + # not sure if all below options are really needed. - "/dev/mem:/dev/mem" - "/dev/gpiomem:/dev/gpiomem" - "/dev/i2c-1:/dev/i2c-1" ports: - - 80:80 + # this is only needed in case applications not running on this device also want to connect to + # this pigpiod daemon. + - 8888:8888 + diff --git a/node-red/Dockerfile.template b/node-red/Dockerfile.template index 4562eb4..9c11121 100644 --- a/node-red/Dockerfile.template +++ b/node-red/Dockerfile.template @@ -6,7 +6,8 @@ FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-node:12-stretch-build as build RUN JOBS=MAX npm install -g --production --unsafe-perm \ node-red \ node-red-admin \ - node-red-contrib-balena + node-red-contrib-balena \ + node-red-node-pi-gpiod ### # Runtime image diff --git a/node-red/app/flow/balena_flows.json b/node-red/app/flow/balena_flows.json new file mode 100644 index 0000000..dca9270 --- /dev/null +++ b/node-red/app/flow/balena_flows.json @@ -0,0 +1 @@ +[{"id":"9cc05709.c67948","type":"tab","label":"Supervisor"},{"id":"e40296d5.afa1c8","type":"tab","label":"GPIO","disabled":false,"info":""},{"id":"7b3c835.42a197c","type":"device","z":"9cc05709.c67948","name":"device state","x":290,"y":100,"wires":[["a558696e.8d6a58"]]},{"id":"a558696e.8d6a58","type":"debug","z":"9cc05709.c67948","name":"","active":true,"console":"false","complete":"false","x":530,"y":80,"wires":[]},{"id":"8409e5a5.c42218","type":"inject","z":"9cc05709.c67948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":80,"wires":[["7b3c835.42a197c"]]},{"id":"71dc1612.c5a5b8","type":"ping","z":"9cc05709.c67948","name":"ping supervisor","x":300,"y":160,"wires":[["6ccb806.e54788"]]},{"id":"6ccb806.e54788","type":"debug","z":"9cc05709.c67948","name":"","active":true,"console":"false","complete":"false","x":530,"y":140,"wires":[]},{"id":"33d9d041.abcaf","type":"inject","z":"9cc05709.c67948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":140,"wires":[["71dc1612.c5a5b8"]]},{"id":"d0264e40.37d05","type":"blink","z":"9cc05709.c67948","name":"blink LED","x":280,"y":220,"wires":[["86799168.15833"]]},{"id":"a9c0e5a5.a56b18","type":"inject","z":"9cc05709.c67948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":200,"wires":[["d0264e40.37d05"]]},{"id":"86799168.15833","type":"debug","z":"9cc05709.c67948","name":"","active":true,"console":"false","complete":"false","x":530,"y":200,"wires":[]},{"id":"3a2b2271.e1618e","type":"restart","z":"9cc05709.c67948","name":"restart container","x":300,"y":280,"wires":[["327ce3e4.ef82fc"]]},{"id":"9cfd3e14.edb96","type":"inject","z":"9cc05709.c67948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":260,"wires":[["3a2b2271.e1618e"]]},{"id":"327ce3e4.ef82fc","type":"debug","z":"9cc05709.c67948","name":"","active":true,"console":"false","complete":"false","x":530,"y":260,"wires":[]},{"id":"65836558.590aac","type":"purge","z":"9cc05709.c67948","name":"purge data partition","x":310,"y":400,"wires":[["63c3d14.445573"]]},{"id":"d1e3768f.f61a28","type":"inject","z":"9cc05709.c67948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":320,"wires":[["78f897ea.317e68"]]},{"id":"1e42ca5c.3ea2b6","type":"inject","z":"9cc05709.c67948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":380,"wires":[["65836558.590aac"]]},{"id":"cf8e1b03.362578","type":"debug","z":"9cc05709.c67948","name":"","active":true,"console":"false","complete":"false","x":530,"y":320,"wires":[]},{"id":"63c3d14.445573","type":"debug","z":"9cc05709.c67948","name":"","active":true,"console":"false","complete":"false","x":530,"y":380,"wires":[]},{"id":"78f897ea.317e68","type":"reboot","z":"9cc05709.c67948","name":"reboot device","x":300,"y":340,"wires":[["cf8e1b03.362578"]]},{"id":"9a4a17fb.b0fcf8","type":"inject","z":"9cc05709.c67948","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":440,"wires":[["dc1fc581.286e38"]]},{"id":"dc1fc581.286e38","type":"shutdown","z":"9cc05709.c67948","name":"shutdown device","x":310,"y":460,"wires":[["ba67e108.0a268"]]},{"id":"ba67e108.0a268","type":"debug","z":"9cc05709.c67948","name":"","active":true,"console":"false","complete":"false","x":530,"y":440,"wires":[]},{"id":"43ab7b2e.f479c4","type":"debug","z":"e40296d5.afa1c8","name":"PIN: 31","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":360,"y":120,"wires":[]},{"id":"33097ea6.18f122","type":"pi-gpiod in","z":"e40296d5.afa1c8","name":"","host":"pigpiod","port":8888,"pin":"6","intype":"PUD_OFF","debounce":"25","read":true,"x":110,"y":120,"wires":[["43ab7b2e.f479c4"]]},{"id":"df4263f1.1eb73","type":"comment","z":"e40296d5.afa1c8","name":"Below node listens to GPIO pin 31","info":"","x":200,"y":60,"wires":[]}] \ No newline at end of file diff --git a/node-red/app/flows/balena.json b/node-red/app/flows/balena.json deleted file mode 100644 index 7d8b0d5..0000000 --- a/node-red/app/flows/balena.json +++ /dev/null @@ -1,264 +0,0 @@ -[{ - "id": "9cc05709.c67948", - "type": "tab", - "label": "Supervisor" -}, { - "id": "7b3c835.42a197c", - "type": "device", - "z": "9cc05709.c67948", - "name": "device state", - "x": 290, - "y": 100, - "wires": [ - ["a558696e.8d6a58"] - ] -}, { - "id": "a558696e.8d6a58", - "type": "debug", - "z": "9cc05709.c67948", - "name": "", - "active": true, - "console": "false", - "complete": "false", - "x": 530, - "y": 80, - "wires": [] -}, { - "id": "8409e5a5.c42218", - "type": "inject", - "z": "9cc05709.c67948", - "name": "", - "topic": "", - "payload": "", - "payloadType": "date", - "repeat": "", - "crontab": "", - "once": false, - "x": 100, - "y": 80, - "wires": [ - ["7b3c835.42a197c"] - ] -}, { - "id": "71dc1612.c5a5b8", - "type": "ping", - "z": "9cc05709.c67948", - "name": "ping supervisor", - "x": 300, - "y": 160, - "wires": [ - ["6ccb806.e54788"] - ] -}, { - "id": "6ccb806.e54788", - "type": "debug", - "z": "9cc05709.c67948", - "name": "", - "active": true, - "console": "false", - "complete": "false", - "x": 530, - "y": 140, - "wires": [] -}, { - "id": "33d9d041.abcaf", - "type": "inject", - "z": "9cc05709.c67948", - "name": "", - "topic": "", - "payload": "", - "payloadType": "date", - "repeat": "", - "crontab": "", - "once": false, - "x": 100, - "y": 140, - "wires": [ - ["71dc1612.c5a5b8"] - ] -}, { - "id": "d0264e40.37d05", - "type": "blink", - "z": "9cc05709.c67948", - "name": "blink LED", - "x": 280, - "y": 220, - "wires": [ - ["86799168.15833"] - ] -}, { - "id": "a9c0e5a5.a56b18", - "type": "inject", - "z": "9cc05709.c67948", - "name": "", - "topic": "", - "payload": "", - "payloadType": "date", - "repeat": "", - "crontab": "", - "once": false, - "x": 100, - "y": 200, - "wires": [ - ["d0264e40.37d05"] - ] -}, { - "id": "86799168.15833", - "type": "debug", - "z": "9cc05709.c67948", - "name": "", - "active": true, - "console": "false", - "complete": "false", - "x": 530, - "y": 200, - "wires": [] -}, { - "id": "3a2b2271.e1618e", - "type": "restart", - "z": "9cc05709.c67948", - "name": "restart container", - "x": 300, - "y": 280, - "wires": [ - ["327ce3e4.ef82fc"] - ] -}, { - "id": "9cfd3e14.edb96", - "type": "inject", - "z": "9cc05709.c67948", - "name": "", - "topic": "", - "payload": "", - "payloadType": "date", - "repeat": "", - "crontab": "", - "once": false, - "x": 100, - "y": 260, - "wires": [ - ["3a2b2271.e1618e"] - ] -}, { - "id": "327ce3e4.ef82fc", - "type": "debug", - "z": "9cc05709.c67948", - "name": "", - "active": true, - "console": "false", - "complete": "false", - "x": 530, - "y": 260, - "wires": [] -}, { - "id": "65836558.590aac", - "type": "purge", - "z": "9cc05709.c67948", - "name": "purge data partition", - "x": 310, - "y": 400, - "wires": [ - ["63c3d14.445573"] - ] -}, { - "id": "d1e3768f.f61a28", - "type": "inject", - "z": "9cc05709.c67948", - "name": "", - "topic": "", - "payload": "", - "payloadType": "date", - "repeat": "", - "crontab": "", - "once": false, - "x": 100, - "y": 320, - "wires": [ - ["78f897ea.317e68"] - ] -}, { - "id": "1e42ca5c.3ea2b6", - "type": "inject", - "z": "9cc05709.c67948", - "name": "", - "topic": "", - "payload": "", - "payloadType": "date", - "repeat": "", - "crontab": "", - "once": false, - "x": 100, - "y": 380, - "wires": [ - ["65836558.590aac"] - ] -}, { - "id": "cf8e1b03.362578", - "type": "debug", - "z": "9cc05709.c67948", - "name": "", - "active": true, - "console": "false", - "complete": "false", - "x": 530, - "y": 320, - "wires": [] -}, { - "id": "63c3d14.445573", - "type": "debug", - "z": "9cc05709.c67948", - "name": "", - "active": true, - "console": "false", - "complete": "false", - "x": 530, - "y": 380, - "wires": [] -}, { - "id": "78f897ea.317e68", - "type": "reboot", - "z": "9cc05709.c67948", - "name": "reboot device", - "x": 300, - "y": 340, - "wires": [ - ["cf8e1b03.362578"] - ] -}, { - "id": "9a4a17fb.b0fcf8", - "type": "inject", - "z": "9cc05709.c67948", - "name": "", - "topic": "", - "payload": "", - "payloadType": "date", - "repeat": "", - "crontab": "", - "once": false, - "x": 100, - "y": 440, - "wires": [ - ["dc1fc581.286e38"] - ] -}, { - "id": "dc1fc581.286e38", - "type": "shutdown", - "z": "9cc05709.c67948", - "name": "shutdown device", - "x": 310, - "y": 460, - "wires": [ - ["ba67e108.0a268"] - ] -}, { - "id": "ba67e108.0a268", - "type": "debug", - "z": "9cc05709.c67948", - "name": "", - "active": true, - "console": "false", - "complete": "false", - "x": 530, - "y": 440, - "wires": [] -}] diff --git a/node-red/app/settings.js b/node-red/app/settings.js index d94ca06..e241957 100644 --- a/node-red/app/settings.js +++ b/node-red/app/settings.js @@ -20,7 +20,7 @@ module.exports = { // the tcp port that the Node-RED web server is listening on - uiPort: process.env.PORT || 80, + uiPort: process.env.PORT || 1880, // By default, the Node-RED UI accepts connections on all IPv4 interfaces. // The following property can be used to listen on a specific interface. For @@ -48,7 +48,7 @@ module.exports = { debugMaxLength: 1000, // The file containing the flows. If not set, it defaults to flows_.json - //flowFile: 'flows.json', + flowFile: 'balena_flows.json', // To enabled pretty-printing of the flow within the flow file, set the following // property to true: diff --git a/node-red/app/start.sh b/node-red/app/start.sh index 578f5a1..40acbfe 100644 --- a/node-red/app/start.sh +++ b/node-red/app/start.sh @@ -1,8 +1,16 @@ #!/bin/bash -# Make the default flows available in the user library -mkdir -p /data/node-red/user/lib/flows || true -cp /usr/src/app/flows/* /data/node-red/user/lib/flows/ +# Make the flow balena_flows.json available in the user library +mkdir -p /data/node-red/user || true + +# only copy the flow balena_flows.json if it doesn't exist as we don't want to overwrite any +# changes made via the node-red editor. +if [ ! -f /data/node-red/user/balena_flows.json ]; then + echo "Copying balena_flows.json from repository to folder /data/node-red/user" + cp /usr/src/app/flow/balena_flows.json /data/node-red/user/balena_flows.json +else + echo "NOT copying balena_flows.json from repository to folder /data/node-red/user as this flow already exists in that folder !!" +fi # Start app DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket node-red --settings /usr/src/app/settings.js diff --git a/pigpiod/Dockerfile.template b/pigpiod/Dockerfile.template new file mode 100644 index 0000000..636efd1 --- /dev/null +++ b/pigpiod/Dockerfile.template @@ -0,0 +1,12 @@ +FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-node:12-stretch-run + +RUN apt-get update && \ + apt-get install -yq --no-install-recommends \ + pigpio \ + python-pigpio && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy and Start app +ADD start.sh /start.sh +CMD ["bash", "/start.sh"] \ No newline at end of file diff --git a/pigpiod/start.sh b/pigpiod/start.sh new file mode 100644 index 0000000..f686de9 --- /dev/null +++ b/pigpiod/start.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +echo "Checking who is listening/bound to port 8888 ..." +netstat -tlep | grep 8888 +if [ "$?" = 0 ]; then + echo "Some process is still listening/bound to that port" + echo "This process needs to be stopped first !" + echo "Sleeping for 20 sec - we hope that this releases the bound TCP port 8888 ..." + sleep 20 + echo "... sleeping finished" +else + echo "...OK, no process is listing/bound to port 8888" +fi + +echo "Starting daemon pigpiod ..." +# for pigpiod options see http://abyz.me.uk/rpi/pigpio/pigpiod.html +# option -g will run the deamon in the foreground. +# option -a 1 should assure that we are not using GPU memory. +/usr/bin/pigpiod -g -a 1 +echo "... daemon stopped unexpectedly." + +# added extra logic for issue: https://github.com/janvda/balena-node-red/issues/1 +echo "netstat -tlep | grep 8888" +netstat -tlep | grep 8888 +echo "sleeping for 20 sec - we hope that this releases the bound TCP port 8888" +sleep 20 +echo "end of sleep" +echo "netstat -tlep | grep 8888" +netstat -tlep | grep 8888 + +echo "... start.sh terminated"