Skip to content

Manage Node-RED and UIbuilder through GitHub

github-badge nodered-badge

In short

In this tutorial we show you how to set up a simple yet effective way to deploy Node-RED flows and settings with GitHub using qbee.io's powerful configuration management.

The general idea is to build a pipeline, where your specific Node-RED flows, credentials, settings and modules can be managed and deployed to a large number of devices. It is also shown how to distinguish between different flows/settings for different customers, devices or locations. As a side note we also install the alternative Node-RED userinterface UIbuilder and show how to manage the user interface.

qbee.io is used to build a secure and repeatable deployment process that allows to manage large amounts of Node-RED devices in production.

Infrastructure

Any commit to the GitHub repository will trigger qbee.io to distribute files that changed to all devices in scope. GitHub communicates with qbee via GitHub runner (using GitHub actions) through the qbee API. This delivers files to the file manager and from there they are rolled out automatically using file distribution.

Core services

  • Node-RED: set up a flow that utilizes a credentials function
  • GitHub: Used as a repository for versioning and triggering new deployments
  • qbee: embedded Linux device management

Setup

Node-RED has a specific directory structure. Per default the root Node-RED directory is in your user's home folder under ".node-red". For this project we use the UIbuilder UI instead of the standard one. This will create an additional directory called uibuilder with some subdirectories. The directory structure is as follows:

directory structure for .node-red
|-- .node-red
|   `|-- flows.json
|    |-- flows_cred.json
|    |-- settings.js
|    |-- package.json
|    |-- node_modules
|    |-- uibuilder
|        |-- common
|        |    |-- images
|        |-- uibuilder
|             |-- dist 
|             |-- src
|                 |-- index.html 
|                 |-- index.js  

In GitHub we will create two independent repositories. One for the basic flow and one for the UIbuilder user interface. Both will be delivered as a tar file. More information follows below.

Node-RED base installation

We assume that you have Node-RED installed. If not you can find a tutorial on how to do this how to automate a headless Node_RED install here.

Adding additional Node-RED modules

This can be done in many different ways, including with qbee templating. In order to keep it simple we use a simple script that installs new npm modules. As an example we install UIbuilder. If more modules are needed these can be added. In our case we also need the vue and bootstrap-vue nodes. The script looks as follows and will later be distributed through qbee's own file distribution. It will install the packages and then restart Node-RED to make them active. Please note that you also can define a specific version like this: npm install [package]@[version]

install additional Node-RED modules with install-modules.sh
    #!/bin/sh

    # make sure you are in the right directory to run the npm installs

    cd /home/pi/.node-red/

    # add the modules you want to install and put this script into your standard .node-red path
    sudo -u pi npm i --unsafe-perm --save --no-progress --no-update-notifier --no-audit --no-fund node-red-contrib-uibuilder


    # Dependencies uibuilder needs
    sudo -u pi npm i --unsafe-perm --save --no-progress --no-update-notifier --no-audit --no-fund  install vue@"2.*" bootstrap-vue@"2.*"  

    # restart Node-RED
    sudo -u pi node-red-restart 

Node-RED flows

Essentially the Node-RED configuration requires a flow file and settings. Below you can find our example flow. This flow sends a value into a function block and then receives a message with a temperature reading out of it that is fed to an MQTT connection with the MQTT out node. The reason we use this is that we want to make sure that we use the credentials functionality which will store encrypted credentials in the flows_cred.json file based on the secret password given in settings.js.

basic-nodered-flow

basic Node-RED example flows.json
    [
    {
        "id": "5d23c98c.7b266",
        "type": "tab",
        "label": "Flow 1",
        "disabled": false,
        "info": ""
    },
    {
        "id": "5d6ae0e7.c3b5e8",
        "type": "inject",
        "z": "5d23c98c.7b266",
        "name": "temp",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "10",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "temp",
        "payload": "20",
        "payloadType": "num",
        "x": 160,
        "y": 256,
        "wires": [
            [
                "2f10cd67.eced22"
            ]
        ]
    },
    {
        "id": "2f10cd67.eced22",
        "type": "function",
        "z": "5d23c98c.7b266",
        "name": "temperature",
        "func": "var payload = msg.payload;\n\nmsg.payload=\"The temperature is \"+msg.payload;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 365,
        "y": 255,
        "wires": [
            [
                "37951f74.afcd6",
                "6aaa640f.8634cc"
            ]
        ]
    },
    {
        "id": "37951f74.afcd6",
        "type": "debug",
        "z": "5d23c98c.7b266",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 591,
        "y": 261,
        "wires": []
    },
    {
        "id": "6aaa640f.8634cc",
        "type": "mqtt out",
        "z": "5d23c98c.7b266",
        "name": "cloudmqtt",
        "topic": "weather",
        "qos": "",
        "retain": "",
        "broker": "31539024.dca0f8",
        "x": 608.5,
        "y": 350,
        "wires": []
    },
    {
        "id": "31539024.dca0f8",
        "type": "mqtt-broker",
        "name": "CloudMQTT",
        "broker": "m77.cloudmqtt.com",
        "port": "14811",
        "clientid": "",
        "usetls": false,
        "compatmode": true,
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "false",
        "birthPayload": "",
        "closeTopic": "",
        "closeQos": "0",
        "closeRetain": "false",
        "closePayload": "",
        "willTopic": "",
        "willQos": "0",
        "willRetain": "false",
        "willPayload": ""
    }
]

In order to make life simple for you we also provide the settings.js here but you can use your own. Put the flow and the settings into your local Node-RED folder. Run Node-RED and point the MQTT block to your MQTT broker. Then set your MQTT credentials. This will create a flows_cred.json with your credentials encrypted with the password defined in settings. If you don't want to test credentials and do not have a MQTT broker you just delete the node and deploy again.

Do not overwrite your own settings.js

If you have your own settings.js file with your own credentialSecret then do not overwrite that. Otherwise your nodes will not be able to resolve their secret credentials.

settings.js for our example
[/**
 * This is the default settings file provided by Node-RED.
 *
 * It can contain any valid JavaScript code that will get run when Node-RED
 * is started.
 *
 * Lines that start with // are commented out.
 * Each entry should be separated from the entries above and below by a comma ','
 *
 * For more information about individual settings, refer to the documentation:
 *    https://nodered.org/docs/user-guide/runtime/configuration
 **/

module.exports = {
    // the tcp port that the Node-RED web server is listening on
    uiPort: process.env.PORT || 1880,

    // By default, the Node-RED UI accepts connections on all IPv4 interfaces.
    // To listen on all IPv6 addresses, set uiHost to "::",
    // The following property can be used to listen on a specific interface. For
    // example, the following would only allow connections from the local machine.
    //uiHost: "127.0.0.1",

    // Retry time in milliseconds for MQTT connections
    mqttReconnectTime: 15000,

    // Retry time in milliseconds for Serial port connections
    serialReconnectTime: 15000,

    // Retry time in milliseconds for TCP socket connections
    //socketReconnectTime: 10000,

    // Timeout in milliseconds for TCP server socket connections
    //  defaults to no timeout
    //socketTimeout: 120000,

    // Maximum number of messages to wait in queue while attempting to connect to TCP socket
    //  defaults to 1000
    //tcpMsgQueueSize: 2000,

    // Timeout in milliseconds for HTTP request connections
    //  defaults to 120 seconds
    //httpRequestTimeout: 120000,

    // The maximum length, in characters, of any message sent to the debug sidebar tab
    debugMaxLength: 1000,

    // The maximum number of messages nodes will buffer internally as part of their
    // operation. This applies across a range of nodes that operate on message sequences.
    //  defaults to no limit. A value of 0 also means no limit is applied.
    //nodeMessageBufferMaxLength: 0,

    // To disable the option for using local files for storing keys and certificates in the TLS configuration
    //  node, set this to true
    //tlsConfigDisableLocalFiles: true,

    // Colourise the console output of the debug node
    //debugUseColors: true,

    // The file containing the flows. If not set, it defaults to flows_<hostname>.json
    flowFile: 'flows.json',

    // To enabled pretty-printing of the flow within the flow file, set the following
    //  property to true:
    //flowFilePretty: true,

    // By default, credentials are encrypted in storage using a generated key. To
    // specify your own secret, set the following property.
    // If you want to disable encryption of credentials, set this property to false.
    // Note: once you set this property, do not change it - doing so will prevent
    // node-red from being able to decrypt your existing credentials and they will be
    // lost.
    credentialSecret: "qbee",

    // By default, all user data is stored in a directory called `.node-red` under
    // the user's home directory. To use a different location, the following
    // property can be used
    //userDir: '/home/nol/.node-red/',

    // Node-RED scans the `nodes` directory in the userDir to find local node files.
    // The following property can be used to specify an additional directory to scan.
    //nodesDir: '/home/nol/.node-red/nodes',

    // By default, the Node-RED UI is available at http://localhost:1880/
    // The following property can be used to specify a different root path.
    // If set to false, this is disabled.
    //httpAdminRoot: '/admin',

    // Some nodes, such as HTTP In, can be used to listen for incoming http requests.
    // By default, these are served relative to '/'. The following property
    // can be used to specifiy a different root path. If set to false, this is
    // disabled.
    //httpNodeRoot: '/red-nodes',

    // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot',
    // to apply the same root to both parts.
    //httpRoot: '/red',

    // When httpAdminRoot is used to move the UI to a different root path, the
    // following property can be used to identify a directory of static content
    // that should be served at http://localhost:1880/.
    //httpStatic: '/home/nol/node-red-static/',

    // The maximum size of HTTP request that will be accepted by the runtime api.
    // Default: 5mb
    //apiMaxLength: '5mb',

    // If you installed the optional node-red-dashboard you can set it's path
    // relative to httpRoot
    //ui: { path: "ui" },

    // Securing Node-RED
    // -----------------
    // To password protect the Node-RED editor and admin API, the following
    // property can be used. See http://nodered.org/docs/security.html for details.
    //adminAuth: {
    //    type: "credentials",
    //    users: [{
    //        username: "admin",
    //        password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.",
    //        permissions: "*"
    //    }]
    //},

    // To password protect the node-defined HTTP endpoints (httpNodeRoot), or
    // the static content (httpStatic), the following properties can be used.
    // The pass field is a bcrypt hash of the password.
    // See http://nodered.org/docs/security.html#generating-the-password-hash
    //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
    //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},

    // The following property can be used to enable HTTPS
    // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
    // for details on its contents.
    // This property can be either an object, containing both a (private) key and a (public) certificate,
    // or a function that returns such an object:
    //// https object:
    //https: {
    //  key: require("fs").readFileSync('privkey.pem'),
    //  cert: require("fs").readFileSync('cert.pem')
    //},
    ////https function:
    // https: function() {
    //     // This function should return the options object, or a Promise
    //     // that resolves to the options object
    //     return {
    //         key: require("fs").readFileSync('privkey.pem'),
    //         cert: require("fs").readFileSync('cert.pem')
    //     }
    // },

    // The following property can be used to refresh the https settings at a
    // regular time interval in hours.
    // This requires:
    //   - the `https` setting to be a function that can be called to get
    //     the refreshed settings.
    //   - Node.js 11 or later.
    //httpsRefreshInterval : 12,

    // The following property can be used to cause insecure HTTP connections to
    // be redirected to HTTPS.
    //requireHttps: true,

    // The following property can be used to disable the editor. The admin API
    // is not affected by this option. To disable both the editor and the admin
    // API, use either the httpRoot or httpAdminRoot properties
    //disableEditor: false,

    // The following property can be used to configure cross-origin resource sharing
    // in the HTTP nodes.
    // See https://github.com/troygoode/node-cors#configuration-options for
    // details on its contents. The following is a basic permissive set of options:
    //httpNodeCors: {
    //    origin: "*",
    //    methods: "GET,PUT,POST,DELETE"
    //},

    // If you need to set an http proxy please set an environment variable
    // called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system.
    // For example - http_proxy=http://myproxy.com:8080
    // (Setting it here will have no effect)
    // You may also specify no_proxy (or NO_PROXY) to supply a comma separated
    // list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk

    // The following property can be used to add a custom middleware function
    // in front of all http in nodes. This allows custom authentication to be
    // applied to all http in nodes, or any other sort of common request processing.
    //httpNodeMiddleware: function(req,res,next) {
    //    // Handle/reject the request, or pass it on to the http in node by calling next();
    //    // Optionally skip our rawBodyParser by setting this to true;
    //    //req.skipRawBodyParser = true;
    //    next();
    //},


    // The following property can be used to add a custom middleware function
    // in front of all admin http routes. For example, to set custom http
    // headers
    // httpAdminMiddleware: function(req,res,next) {
    //    // Set the X-Frame-Options header to limit where the editor
    //    // can be embedded
    //    //res.set('X-Frame-Options', 'sameorigin');
    //    next();
    // },

    // The following property can be used to pass custom options to the Express.js
    // server used by Node-RED. For a full list of available options, refer
    // to http://expressjs.com/en/api.html#app.settings.table
    //httpServerOptions: { },

    // The following property can be used to verify websocket connection attempts.
    // This allows, for example, the HTTP request headers to be checked to ensure
    // they include valid authentication information.
    //webSocketNodeVerifyClient: function(info) {
    //    // 'info' has three properties:
    //    //   - origin : the value in the Origin header
    //    //   - req : the HTTP request
    //    //   - secure : true if req.connection.authorized or req.connection.encrypted is set
    //    //
    //    // The function should return true if the connection should be accepted, false otherwise.
    //    //
    //    // Alternatively, if this function is defined to accept a second argument, callback,
    //    // it can be used to verify the client asynchronously.
    //    // The callback takes three arguments:
    //    //   - result : boolean, whether to accept the connection or not
    //    //   - code : if result is false, the HTTP error status to return
    //    //   - reason: if result is false, the HTTP reason string to return
    //},

    // The following property can be used to seed Global Context with predefined
    // values. This allows extra node modules to be made available with the
    // Function node.
    // For example,
    //    functionGlobalContext: { os:require('os') }
    // can be accessed in a function block as:
    //    global.get("os")
    functionGlobalContext: {
        // os:require('os'),
        // jfive:require("johnny-five"),
        // j5board:require("johnny-five").Board({repl:false})
    },
    // `global.keys()` returns a list of all properties set in global context.
    // This allows them to be displayed in the Context Sidebar within the editor.
    // In some circumstances it is not desirable to expose them to the editor. The
    // following property can be used to hide any property set in `functionGlobalContext`
    // from being list by `global.keys()`.
    // By default, the property is set to false to avoid accidental exposure of
    // their values. Setting this to true will cause the keys to be listed.
    exportGlobalContextKeys: false,


    // Context Storage
    // The following property can be used to enable context storage. The configuration
    // provided here will enable file-based context that flushes to disk every 30 seconds.
    // Refer to the documentation for further options: https://nodered.org/docs/api/context/
    //
    //contextStorage: {
    //    default: {
    //        module:"localfilesystem"
    //    },
    //},

    // The following property can be used to order the categories in the editor
    // palette. If a node's category is not in the list, the category will get
    // added to the end of the palette.
    // If not set, the following default order is used:
    //paletteCategories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'],

    // Configure the logging output
    logging: {
        // Only console logging is currently supported
        console: {
            // Level of logging to be recorded. Options are:
            // fatal - only those errors which make the application unusable should be recorded
            // error - record errors which are deemed fatal for a particular request + fatal errors
            // warn - record problems which are non fatal + errors + fatal errors
            // info - record information about the general running of the application + warn + error + fatal errors
            // debug - record information which is more verbose than info + info + warn + error + fatal errors
            // trace - record very detailed logging + debug + info + warn + error + fatal errors
            // off - turn off all logging (doesn't affect metrics or audit)
            level: "info",
            // Whether or not to include metric events in the log output
            metrics: false,
            // Whether or not to include audit events in the log output
            audit: false
        }
    },

    // Customising the editor
    editorTheme: {
        projects: {
            // To enable the Projects feature, set this value to true
            enabled: false
        }
    }
}

If you have all these files in your local Node-RED folder and you can send data to your MQTT server we can move to the next step and create a GitHub repository.

GitHub

On GitHub we will set up two different repositories. One for managing the Node-RED flows and one for working with the UIbuilder files. If you do not use UIbuilder you can just disregard the second step.

How to create a repository on GitHub

Check out the official GitHub documentation on how to create a repository.

The established GitHub repository looks like this. You can either upload files through the UI or with the git command line tools.

github-node-red

This is the automatated workflow that is triggered by any new commit:

  • use git for version control management
  • setup a GitHub runner (using GitHub actions) to create a tarball once changes are made.
  • copy the tarball to the qbee.io file manager via API calls. Path is /node-red-demo.
  • use the qbee.io file distribution to upload the new file to a list of devices and to restart Node-RED

Once a repository is set up, we specify the GitHub secrets as shown in the following screenshot. We define the qbee login mail address as QM and the password as QP. This will not be exposed.

github-secrets

Now you can work with GitHub actions using the secrets defined before. You can run your script on a runner. These runners can be provided by GitHub. Alternatively, you can use your own machines. Clicking on "set up a workflow yourself" you can work with these workflows and actions.

The script we used is the following:

node-red-flow-distribution.yml
name: Automated Node-RED flow file distribution

on:
 push:
    branches: [ main ]
 pull_request:
    branches: [ main ]

jobs:  


 build:
    runs-on: ubuntu-latest
    env:
        TARNAME: node-red-files.tar   

    steps:
    - uses: actions/checkout@v2

    - name: install jq
      run: |
        sudo apt-get install jq

    - name: create tarball
      run: |
        mkdir ./tar
        tar --exclude='./.git' --exclude='README.md' --exclude='./.github' --exclude='./tar' -czvf ./tar/$TARNAME .

    - name: copy to qbee file manager
      run: |
        successful_status_code='200'
        auth='false'
        i=0
        token=""
        while [[ "$auth" != "true" && $i -lt 10 ]]
        do
            echo "try number $((i++))"
            output=$(curl --request POST -sL --url 'https://www.app.qbee.io/api/v2/login' \
          --header "Content-Type: application/json" \
          -d "{\"email\":\"${{ secrets.QM }}\",\"password\":\"${{ secrets.QP }}\"}"\
          -w "\n{\"http_code\":%{http_code}}\n")
            http_code=$(echo $output | jq -cs | jq -r '.[1].http_code')
            echo $http_code
            tokenValue=$(echo $output | jq -cs | jq -r '.[0].token')
            echo $tokenValue
            if [ "$http_code" != "$successful_status_code" ]
            then
                echo $tokenValue
            #uncomment next line to do nothing
            #: 
            else
                auth='true'
                apiOutput=$(curl -i --request "DELETE" -d "path=/node-red-demo/$TARNAME" -H "Content-type: application/x-www-form-urlencoded" \
                   --url 'https://www.app.qbee.io/api/v2/file'\
                   --header 'Authorization: Bearer '"$tokenValue")
                echo "API output is:\n$apiOutput"
                apiOutput=$(curl -i --request POST -H "Content-Type:multipart/form-data" -F "path=/node-red-demo/" -F "file=@./tar/$DOCKERNAME" \
                   --url 'https://www.app.qbee.io/api/v2/file'\
                   --header 'Authorization: Bearer '"$tokenValue")
                echo "API output is:\n$apiOutput"
            fi
        done    

Comments on the yaml file

  • this action is triggered on pushes to the main branch
  • the runner is a virtual machine with an ubuntu OS as we specified ubuntu-latest
  • a clean virtual machine is spin up on every run checking out the code from the repository with the actions/checkout@v2 option
  • For the API JWT authentication is needed and the jq package provides all necessary code for that
  • the output is placed into the root directory of your qbee.io file manager (c.f. file distribution via API)

There are no credentials in the action output:

github-node-red-secrets

File distribution to embedded devices

Finally, we distribute our files to the remote devices as usual with the integrated file distribution. Here we can decide if we want to distribute to all devices, to groups or to a specific device. You can do this operation with multiple flows moving different tar files into different groups of devices.

Recent change for GitHub runner

GitHub has changed the way the runners work. Now they perform tasks in the scope of a docker user. Then the "tar" command will untar as docker user on the device. To prevent this we add the option -o to the command options to override this.

As a "command to run" we use:

 sudo -u pi tar -xvoz /home/pi/.node-red/node-red-files.tar -C /home/pi/.node-red  && sudo -u pi node-red-restart 
This uncompresses the tar file. This needs to run as user pi and since we are in root context we need to define the proper target directory with -C /home/pi/.node-red (or it will end up in root). The following command sudo -u pi node-red-restart restarts Node-RED with user context "pi".

file-distribution-node-red

Now Node-RED will install the flows fully automated and restart Node-RED. This leaves us with one optional step: Install additional modules.

In oder to do that we include the script explained above and put that into the file manager.

install additional Node-RED modules with install-modules.sh
    #!/bin/sh

    # make sure you are in the right directory to run the npm installs

    cd /home/pi/.node-red/

    # add the modules you want to install and put this script into your standard .node-red path
    sudo -u pi npm i --unsafe-perm --save --no-progress --no-update-notifier --no-audit --no-fund node-red-contrib-uibuilder


    # Dependencies uibuilder needs
    sudo -u pi npm i --unsafe-perm --save --no-progress --no-update-notifier --no-audit --no-fund  install vue@"2.*" bootstrap-vue@"2.*"  

    # restart Node-RED
    sudo -u pi node-red-restart 

Then it is added to the file distribution. It is run as well as user pi with the following command: chmod +x /home/pi/.node-red/install-modules.sh && sudo -u pi /home/pi/.node-red/install-modules.sh

This makes the script user executable and then runs it.

file-distribution-node-red2

UIbuilder

Now UIBuilder will work and the VueJS libraries are installed. You can also use JQuery, VueJS, MoonJS, REACT, UmbrellaJS and Riot as well as AngularJS. It can be seen on the /uibuilder URL. You can easily access your remote device with qbee-connect from your local desktop PC.

The thing remaining is to create another github repository that is able to handle the UI part of your Node-RED project with UI builder.

The way this works is very similar to what we did with Node-RED.

  • create a GitHub repository
  • create a github runner that exports changes to the qbee file manager (again as a tar file)
  • create a file distribution that delivers and unpacks the tarball
  • restart Node-RED with the same "command to run" that also unpacked the tarball.

In the github repository for the UIbuilder files the structure of the uibuilder folder needs to be modelled.

github-uibuilder

If you only work in uibuilder/uibuilder/src then you only need to upload and manage this through github. Please note that their is a .config file in the main uibuilder directory. This is excluded from being overwritten by the untar process through an exclude --exclude=".config".

Here is the GitHub runner script that delivers the tarball to qbee. Don't forget to define your secret QM and QP as explained above:

uibuilder-to-qbee.yml
        name: Automated Uibuilder file distribution

    on:
     push:
        branches: [ main ]
     pull_request:
        branches: [ main ]

    jobs:  


     build:
        runs-on: ubuntu-latest
        env:
            TARNAME: uibuilder.tar   

        steps:
        - uses: actions/checkout@v2

        - name: create tarball
          run: |
            mkdir ./tar
            tar --exclude='./.git' --exclude='README.md' --exclude='./.github' --exclude='./tar' -czvf ./tar/$TARNAME .
        - name: copy to qbee file manager
          run: |
            curl --digest -u ${{ secrets.QM }}:${{ secrets.QP }} -i -X "DELETE" -d "path=/node-red-demo/$TARNAME" -H "Content-type: application/x-www-form-urlencoded" https://www.app.qbee.io/api/v2/file
            curl --digest -u ${{ secrets.QM }}:${{ secrets.QP }} -i -X POST -H "Content-Type:multipart/form-data" -F "path=/node-red-demo/" -F "file=@./tar/$TARNAME" https://www.app.qbee.io/api/v2/file

Next step is to extend file distribution such that the created tarball will be distributed into /home/pi/.node-red/uibuilder. The command to run is then (including the exclude command):

    sudo -u pi tar -xvoz /home/pi/.node-red/uibuilder/uibuilder.tar -C /home/pi/.node-red/uibuilder --exclude=".config"  && sudo -u pi node-red-restart 

The resulting full file distribution looks like this and can be imported through the UI:

full-file-distribution.json
    {
  "enabled": true,
  "files": [
    {
      "templates": [
        {
          "source": "/node-red-demo/node-red-files.tar",
          "destination": "/home/pi/.node-red/node-red-files.tar",
          "is_template": false
        }
      ],
      "command": "sudo -u pi tar -xvzf /home/pi/.node-red/node-red-files.tar -C /home/pi/.node-red > /dev/null 2>&1 && sudo -u pi node-red-restart > /dev/null 2>&1"
    },
    {
      "templates": [
        {
          "source": "/node-red-demo/install-modules.sh",
          "destination": "/home/pi/.node-red/install-modules.sh",
          "is_template": false
        }
      ],
      "command": "chmod +x /home/pi/.node-red/install-modules.sh && su - pi -c /home/pi/.node-red/install-modules.sh > /home/pi/.node-red/output.log"
    },
    {
      "templates": [
        {
          "source": "/node-red-demo/uibuilder.tar",
          "destination": "/home/pi/.node-red/uibuilder/uibuilder.tar",
          "is_template": false
        }
      ],
      "command": "sudo -u pi tar -xvzf /home/pi/.node-red/uibuilder/uibuilder.tar -C /home/pi/.node-red/uibuilder --exclude=\".config\" > /dev/null 2>&1 && sudo -u pi node-red-restart > /dev/null 2>&1"
    }
  ],
  "version": "v1",
  "bundle_commit_id": "a2a4a23ad91cb9c4758d8a9f458684f93f8c56a6d0028db6d39c77a1d55fd3f3"
}

Testing the complete setup

With the current setup you can move Node-RED flows onto any device in scope and update them with any new change. Both settings and credentials are provided as well. Please note that the way we do this is that we use a tarball to deliver this. This means that if we manually deploy a changed flow on any of the devices these flows will stay changed until we commit a master change in GitHub and unpack the tarball again.

Deployment process information

A new GitHub commit creates a tarball which then is rolled out and locally replaces the current flows. This will happen anytime a commit runs. If you want to have full control and protect flows from being locally changed you can control them directly. Please see our GitHub Node-RED tutorial.

Now we want to do some additional tests. The current setup runs a flow that sends a temperature value to a specific MQTT broker. In the testing we will do the following:

  • distribute a new flow that creates a random waveform and feeds that to the uibuilder node
  • pick up and display these values with the standard demo uibuilder UI
  • change the demo uibuilder file in a way that the qbee logo is displayed

This test will show how easy it is to manage both the flows as well as the UIbuilder UI. The first thing we do is that we upload an alternative flows.json to GitHub and commit that.

flow-uibuilder

flows.json
        [
    {
        "id": "5d23c98c.7b266",
        "type": "tab",
        "label": "Flow 1",
        "disabled": false,
        "info": ""
    },
    {
        "id": "37951f74.afcd6",
        "type": "debug",
        "z": "5d23c98c.7b266",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 591,
        "y": 261,
        "wires": []
    },
    {
        "id": "3a253db5.9568ba",
        "type": "inject",
        "z": "5d23c98c.7b266",
        "name": "mymath",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "1",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 220,
        "y": 260,
        "wires": [
            [
                "1823989a.712057"
            ]
        ]
    },
    {
        "id": "a74120d7.476068",
        "type": "function",
        "z": "5d23c98c.7b266",
        "name": "sine + rand",
        "func": "function randn_bm() {\n    let min=-1, max=1\n    let u = 0, v = 0;\n    while(u === 0) u = Math.random(); //Converting [0,1) to (0,1)\n    while(v === 0) v = Math.random();\n    let num = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );\n\n    num = num / 10.0 + 0.5; // Translate to 0 -> 1\n    if (num > 1 || num < 0) num = randn_bm(min, max, skew); // resample between 0 and 1 if out of range\n    //num = Math.pow(num, skew); // Skew\n    num *= max - min; // Stretch to fill range\n    num += min; // offset to min\n    return num;\n}\n\nvar d = Math.sin(Math.PI/2);\nvar s = 0.1\nvar noise = randn_bm(s) * 0.5;\nvar freq = 1/60;\nvar t = msg.payload;\nmsg.payload = Math.sin(2*Math.PI * freq * t ) + noise;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 390,
        "y": 340,
        "wires": [
            [
                "37951f74.afcd6",
                "eeff96cd.023cf8"
            ]
        ]
    },
    {
        "id": "1823989a.712057",
        "type": "function",
        "z": "5d23c98c.7b266",
        "name": "ts to sec",
        "func": "var sec = new Date(msg.payload).getSeconds();\nmsg.payload = sec;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 380,
        "y": 260,
        "wires": [
            [
                "a74120d7.476068"
            ]
        ]
    },
    {
        "id": "eeff96cd.023cf8",
        "type": "uibuilder",
        "z": "5d23c98c.7b266",
        "name": "",
        "topic": "",
        "url": "uibuilder",
        "fwdInMessages": false,
        "allowScripts": false,
        "allowStyles": false,
        "copyIndex": true,
        "showfolder": false,
        "useSecurity": false,
        "sessionLength": 432000,
        "tokenAutoExtend": false,
        "x": 600,
        "y": 340,
        "wires": [
            [],
            []
        ]
    }
]

This will now automatically trigger a new file distribution and Node-RED will be restarted. This flow contains a UIbuilder node which is connected to the random function input. Accordingly, we can see the messages in the UI under the /uibuilder URL.

github-uibuilder-data

As a last step we uploaded a qbee logo into the uibuilder/src folder on github and we edit the index.html file to display it by adding: <img src="./qbee-logo.png" alt="qbee-logo" width="200" class="center"> A commit triggers the action and the distribution onto one or many devices.

This results in a messed up uibuilder boilerplate (sorry about this) but you get the idea what you can do with this and how powerful it is to have both the complete Node-RED flows as well as the alternative Node-RED UI UIbuilder under full repository control:

uibuilder-change

If you have questions, feedback or comments please feel free to reach out to us through the homepage.