From Nodered to Tulip Custom Widget Gauge

Posted here From Nodered to Tulip Custom Widget – Michael Ellerbeck

One way to realtime update a custom Tulip widget (within the Tulip player,or chrome player not test mode) is to make an update to a Tulip Table Record.

Let’s begin

Create a tulip table, lets call it guage_test. And create a record of ID 1 value 50

(I realized I spelled gauge incorrectly on the table, sorry deal with it

To get data into the table lets create a tulip Bot, lets call it table_bot and give it tables:read and write permissions. Save the secret, because you can’t view it again.

Fire up node red and add the node-red-tulip-api to your Palette

Drop the tulip-tables node and then configure the API portion with the values from creating the BOT.

Now set it to update a record, you will need a Table ID. Easiest way I know is to get it from the Url of the table. Copy the unique id (everything after table/) and paste into Table ID

And set the Record ID to 1, you could do this dynamically of course, this is just a simple demo.

Next we need to get the field name, it will be a unique id appended to your field you created. You can use nodered to get this. Simply leave the request body as {} and it will return you the current record.

Add an inject before and a debug after to catch the return message. Sorry tiny image, but it tells us our ‘value’ is referred to by “jnifa_value”

Great, now we can craft the request body

Edit your inject node, change to msg.body then change to output of JSON.

Use the {“name_of_your_value”: 1}

Now if you deploy and click the inject node you should see in the debug area your update record.

If you look at your tulip table it will reflect the new value as well.

Great, so now we have data flowing into a tultip table. Now let’s widgetify it.

Go to the custom widget page and create a new one. I used this cool animated gauge http://codepen.io/naikus/pen/BzkoLL to start

In the html section paste

<div id="gauge6" class="gauge-container six">
  <span class="label">.six</span>
</div>

In the javascript section paste the below. This uses a loadScript to load gauge.min.js async and then calls a ‘main’ function to create the gauge and the getValue function. There is probably a better way to do this (let me know what it is

async function loadScripts(...urls) {
  for (const thisUrl of urls) {
    console.log(`Loading script "${thisUrl}"...`);

    const response = await fetch(thisUrl);
    const responseBody = await response.text();

    const scriptElm = document.createElement("script");
    const inlineCode = document.createTextNode(responseBody);
    scriptElm.appendChild(inlineCode);
    document.body.appendChild(scriptElm);

    console.log(`Script "${thisUrl}" successfully loaded`);
    main();
  }
}

loadScripts('https://rawgit.com/naikus/svg-gauge/master/dist/gauge.min.js');

function main() {
    
var gauge6 = Gauge(
  document.getElementById("gauge6"), {
    max: 100,
    dialStartAngle: 90.01,
    dialEndAngle: 89.99,
    dialRadius: 10,
    showValue: false,
    value: 50
  }
);


getValue('value',(tempVar) => {
    // console.debug(tempVar);
   gauge6.setValueAnimated(tempVar, 1);
})
 
    
}

Finally, paste in the CSS

body {
  background-color: rgba(0,0,0,0.8);
  color: #999;
  font-family: Hevletica, sans-serif;
}

.info {
  clear: both;
  padding: 10px;
  font-size: 0.9em;
}
a.link {
  color: rgb(47, 227, 255);
  text-decoration: none;
}

.warn {
  font-size: 0.8em;
  background-color: darken(orange, 15%);
  color: #fff;
  padding: 10px;
}

/* ------ Default Style ---------- */
.gauge-container {
  width: 150px;
  height: 150px;
  display: block;
  float: left;
  padding: 10px;
  background-color: #222;
  margin: 7px;
  border-radius: 3px;
  position: relative;
}
.gauge-container > .label {
  position: absolute;
  right: 0;
  top: 0;
  display: inline-block;
  background: rgba(0,0,0,0.5);
  font-family: monospace;
  font-size: 0.8em;
  padding: 5px 10px;
}
.gauge-container > .gauge .dial {
  stroke: #334455;
  stroke-width: 2;
  fill: rgba(0,0,0,0);
}
.gauge-container > .gauge .value {
  stroke: rgb(47, 227, 255);
  stroke-width: 2;
  fill: rgba(0,0,0,0);
}
.gauge-container > .gauge .value-text {
  fill: rgb(47, 227, 255);
  font-family: sans-serif;
  font-weight: bold;
  font-size: 0.8em;
}




/* ------- Alternate Style ------- */
.wrapper {
  height: 100px;
  float: left;
  margin: 7px;
  overflow: hidden;
}
.wrapper > .gauge-container {
  margin: 0;
}
.gauge-container.two {
}
.gauge-container.two > .gauge .dial {
  stroke: #334455;
  stroke-width: 10;
}
.gauge-container.two > .gauge .value {
  stroke: orange;
  stroke-dasharray: none;
  stroke-width: 13;
}
.gauge-container.two > .gauge .value-text {
  fill: #ccc;
  font-weight: 100;
  font-size: 1em;
}



/* ------- Alternate Style ------- */
.gauge-container.three {
}
.gauge-container.three > .gauge .dial {
  stroke: #334455;
  stroke-width: 2;
}
.gauge-container.three > .gauge .value {
  stroke: #C9DE3C;
  stroke-width: 5;
}
.gauge-container.three > .gauge .value-text {
  fill: #C9DE3C;
} 



/* ----- Alternate Style ----- */
.gauge-container.four > .gauge .dial {
  stroke: #334455;
  stroke-width: 10;
}
.gauge-container.four > .gauge .value {
  stroke: #F32450;
  stroke-dasharray: none;
  stroke-width: 10;
}
.gauge-container.four > .gauge .value-text {
  fill: #F32450;
  transform: translate3d(26%, 20%, 0);
  display: inline-block;
}
.gauge-container.four .value-text {
  color: #F32450;
  font-weight: 100;
  position: absolute;
  bottom: 18%;
  right: 10%;
  display: inline-block;
} 



/* ----- Alternate Style ----- */
.gauge-container.five > .gauge .dial {
  stroke: #334455;
  stroke-width: 5;
}
.gauge-container.five > .gauge .value {
  stroke: #F8774B;
  stroke-dasharray: 25 1;
  stroke-width: 5;
}
.gauge-container.five > .gauge .value-text {
  fill: #F8774B;
  font-size: 0.7em;
}



/* ----- Alternate Style ----- */
  .gauge-container.six > .gauge .dial {
    stroke: #334455;
    fill: "#334455";
    stroke-width: 20;
  }
  .gauge-container.six > .gauge .value {
    stroke: #F8774B;
    stroke-width: 20;
  }
  .gauge-container.six > .gauge .value-text {
    fill: #FF6DAF;
    font-size: 0.7em;
  }


.gauge-container.seven > .gauge .dial {
  stroke: transparent;
  stroke-width: 5;
  transform: scale(0.9,0.9) translate3d(5.5px, 5.5px, 0);
  fill: rgba(148, 112, 57, 0.42);
}
.gauge-container.seven > .gauge .value {
  stroke: #F8774B;
  stroke-dasharray: none;
  stroke-width: 5;
}

Now, create a prop called value. If you type in a number you should see the gauge update.

Great, now lets create a Tulip App. Make sure to use an emoji in the name because its cool

Lets create a table record

An a trigger on app open

Then drop our Gauge widget onto the app and set the Value prop to the Table Record

Now run the App, this update only works in the tulip player version (or chrome player, not test)

Now, head back to the nodered flow, change the value you are updating and inject it and watch in amazement as the little gauge does an animated change.

1 Like

Really interesting. A way to process events published by other applications and to exploit these events in Tulip applications. Thanks

Fantastic @mellerbeck! This is super cool!

Your approach using getValue to automatically update when your prop updates is exactly the way we intend you to do this.

I have used this approach (using getValue to call a function) to make change detectors that run a trigger when a variable changes. This is super slick in combination with aggregations. You could tie your gauge to an aggregation returning your scrap rate for the day, throughput vs. target, or just about anything else you care about and it would dynamically update. Makes some really interesting dashboard visualizations possible.

Keep messing around, so much is possible!
Pete

Hi All,

it’s been a while since this was active but does anyone know how to flexible fill this in to update records(see image)

Flow is to start a downtime → Create record in a table to log. I currently look for that record ID and have this in my payload. But I can’t seem to leave the field empty of the record id and only send it in the payload…

It seems it is also not working otherwise…

An update would require a Record ID (how else does it know what to update :slight_smile:

Yes, Im aware. Im currently loading that record in a node before, but can’t seem to fill this in in a dynamic way here on screen.
So in the function before I have the ID in my msg.payload, And this is isolated as is. But I can’t seem to fill this in…

I’m I missing something?

So, you are asking a node-RED question?

Yes, referring to ‘And set the Record ID to 1, you could do this dynamically of course, this is just a simple demo.’ from the explanation above

You could ask on the nodeRED forum, they are very friendly folks!