A Deep Learning Weekend

Gauge by Peter ScargillWell, this has been an interesting journey (a deep learning WEEK more like it) as I’ve been working on making my own widgets for Node-Red. I thought I’d share some of the things I’ve picked up. Experienced JavaScript object users please look away now. Those not interested in JavaScript look away now.

Note: The original video for this is out of date as I have learned more – the new video has some extra code, too – here it is - https://youtu.be/mTAPrvB1EeI

C Programmer: As a life-long C programmer, I came into JavaScript with some trepidation. Like alternative universes, JavaScript may look similar to C – but it is not the same – and that led to all manner of silly mistakes over time. However as I see a tiny light at the end of the tunnel, I have to say, it was worth the effort. Despite being a client-side interpreted language, it really is great fun to use.

The gauge: Well, as long as you keep it simple that is. When designing my first attempt at a gauge for Node-Red, I realised that a typical gauge operation goes like this:

Some features such as the background are typically set in an init function once only (and as such it would be better if the user is not involved in this. Other features such as needle positions are set dynamically and will change during operation.

This is generally handled one of two ways – either by setting a value and then calling an update function, or by running something in the background which checks for value changes so all you have to do is change a variable. On many of the gauges you’ll see flashing lights and smooth movement of gauge needles – that implies something running constantly or at least checking for changes and running when change is needed.

Concerns: This worries me as there can be quite some overhead in updating an entire gauge, umpteen times a second. So I decided that what was needed was a layering system as you might find in Photoshop – such that I could lay down the basic design and background of a gauge once only then dynamically clear and write to a top layer whenever changes are required. Such changes might include the state of a flashing light – or a needle not being where you want it, requiring successive incremental updates until the needle or needles do get to be where you want.

I then started thinking about variables and functions (methods). Did I want to update variables such as the needle pointer directly (direct variable manipulation) – or did I want to manipulate the input value before passing it through (that needs  a method/function).

That led me to create, as you will have seen earlier, a large blob of JavaScript for my gauge, including initial setup – then a timer-driven function to update the gauge regularly. That was stage one.

HTML5 Canvas: In the process I discovered (by looking at what others had done) that HTML5 Canvas (which really is easy once you get past the initial learning curve) Is a great choice. What did not come so easily was the realisation that the canvasses themselves are indeed transparent and you can layer one directly on top of another – just like Gimp or Photoshop layers. That led me to split the job into two – the overall backdrop which remains fixed throughout operation – and the top layer with my LEDs, needles and set points on it.

Adding a bunch of variables to this for setup and defaults brought me out into a sweat as I struggled to imagine how two or more gauges would interact. And THAT led me onto objects – something I’d never really touched in JavaScript before  – specifically to wrap up my entire code into a definition – which meant I could then simply create instances for EACH gauge without worrying about all those variables interacting. That was the point I last few days in deep learning.

Having wrapped everything up in a bulletproof package I was then faced with the question of how to access required functions and data externally. The obvious way (and the one I’ve adopted) is a series of callable functions for example:  mygauge1.needle1(50);

In the process I had to learn how to hide functions within the definition and make functions accessible externally. That was easy enough.

Here is a simple example – the assumption is that you have a jQuery link (to update a div) and a DIV called “fred”

1.  function myobj(){
2.  this.v1=5;
3.  init=function(passThis) { setInterval(every25ms, 25,passThis); }
4.  this.doubleit=function(r) { this.v1=r*2; }
5.  every25ms=function (passThis) { $("#fred").html(passThis.v1); }
6.  init(this);
7.  }

8.  brian=new myobj;
9.  //brian.doubleit(12);
10. //brian.v1=7;

There is a lot to this – and I’ll be using the word “this” a lot – so for clarify I’ll use italics for the special meaning of this.  It may look strange at first but if you follow me it soon won’t.

Definition: So the point of this was to create a test object definition – an object that would simply put a value in a DIV (I know, slightly overkill but I was trying to prove a point).  We start off with the definition in line 1.

Take a look at the code above – there is a lot in here despite their being only 9 lines. I figured any more and people might run away.

Firstly on line 1,  we define a type of object. A “myobj” type.  I’m not making anything here – just defining it (ends in line 7).

On line 2 – we define an externally available variable – v1. “this” makes it accessible externally (try removing “this”).

In line 3, we are defining  a function called init.  When the instance is created on line 8, init will be called internally (by line 6) and will start off a timer which will run every 25ms and which will call an internal function called “every25ms” (line 5), passing this to it. Do not get hung up on this yet.

Line 4 defines a function (again made available externally by “this” and called “doubleit”,  will double whatever is passed to it.

The  “every25ms”  function on line 5 which is to be called every 25ms by init… simply prints out the local variable.

Line 6 calls init (which is NOT accessible externally) when the instance is created in line 8 - to initialise the instance and start off the timer – this saves running an init externally.  From line 8 onwards,  the function “every25ms” is called every 25ms to put the value of this.v1 onto the DIV.

You’ll see “5” appear in your DIV if you test this somewhere – that’s the initial value. Note "passThis" – (arbitrary name). Because the function init is private - I don't prefix it with "this" – when it sets the interval timer it has pass to the timer function the outer “this”… - so I pass "this" in the init call.  This is new as a result of feedback in here suggesting a private “init” function that was no accessible externally. In the original version I used this.init() - and passing this worked. Removing that resulted in the timer callback function using the wrong "this".  Trace it through and it makes sense. The callback function needs to know about the outer "this".

Line 8 is where things start to happen. At that point, the instance (brian) is created and the timer starts all on it’s own -  thanks to the internal init function.

So far so good. How do I now interact with that variable v1? I wanted the choice of updating a variable or calling a function. The two commented lines are two ways to do this – their operation should be self-explanatory.

Line 9 simply won’t interact with local functions inside the definition and we don’t want them to – we want interaction with specific functions in each instance of the object – like “brian” for example. And that’s where “this” comes in. Add “this” to a variable name or a function name – and it becomes accessible to the outside world. For example you CANNOT do brian.init() or worse, brian.every25ms() and that’s a good thing!

Scope: So using this technique allowed me to keep all variables and functions private and only “expose” those I needed. LOVELY.  If I only wanted to use function and not expose any variables – I would not use for example “this” as a prefix to v1.

These ideas then form the basis of my current gauge. I can built a massive machine with lots of variables and functions inside and have no worries about interactions with other gauges because I’m only “exposing” stuff I want…. and gauge1.v1 has nothing to do with gauge2.v1

Of course I’m assuming that was as clear as mud -  but if you are interested, go have a play in this CodePen where I’ve left the code. See the number that comes up in the working area – try un-commenting those functions (methods) in lines 9 and 10. See what happens if you replace passThis.v1 with this.v1 or just v1.  See what happens if you remove references to this altogether. CodePen is great as changes run immediately and safely.

Merely experimenting with the code in that CodePen should do what it did for me – clear a massive amount of mental fog.

Simply creating v1 – and removing this and passThis in referring to v1 – leads to disaster – v1 is accessible externally on it’s own –so kiss goodbye to multiple instances.  But the same code using “var v1” leads to a save internal v1 NOT accessible as “brian.v1”. Your choice.

My new gauge object is coming along nicely – and a minimal installation involves little more than a link and a couple of lines of code – stunning how these simple ideas change the whole game.


A week of Learning

Working on the gauge in CodePenSome may be wondering if I’ve gone off on holiday, as I normally post here on an almost daily basis. Well, if you regularly read the comments sections you’ll see I’ve been heavily involved in answering questions, but more importantly I’ve taken one step back and two steps forward on this gauge thing. Read on.

But firstly – WIN SOMETHING! You’ve probably heard me talk about my pal Peter Oakes in Canada – he has an excellent YouTube Channel in which he talks about IOT – well, he’s just launched a competition and the prizes look pretty damned impressive indeed. Take a quick looks over here – you might want to participate…. https://www.hackster.io/contests/IOT2020

Thanks:  Before going any further, I’d like to take this opportunity to thank some people in there without naming names – for several donations received this week – you have no idea how this helps motivation, knowing that your work is appreciated. Thank you, guys!

The Gauge: Now - regular visitors will know I’ve been doing a lot of work on Node-Red-Dashboard-compatible widgets recently as I really do think this is the way forward for mobile access to DIY home control systems and also for desktop and wall access via touch displays -  but one thing that has bothered me up to now was the clumsiness of some of my coding – global variables all over the place, a lot of opportunity for interference from one node to another etc. Everything just works but the perfectionist in me wants to “do the job properly”.

So earlier this week I took a step back, seeing what “proper programmers” had done with turning these widgets into “objects”, with clean interfacing to the outside world. Well, trust me, it isn’t that easy. I asked around as to how I might encapsulate my Javascript into an object, exposing only those bits that needed to be exposed. No-one I know well enough to ask for help, could help in this particular instance! So, I took a step back, grabbed some coffee and thanks to my friend Google, I’m pleased to say I’m getting somewhere.

Shortly you will see my gauge once again featured in these pages but the way it is done and bits you’ll need to look at have been dramatically changed as has the chance of unwanted interaction – and the overhead has been substantially reduced due to a series of insights which have followed each other – and some of them I’m sure some others have missed. Read on.

So first off, I made a gauge with pretty colours – you’ll see it elsewhere in this blog. It works – why take it any further?

Well, the gauge features nice, smoothly moving needles and other bits. What’s wrong with that?  Every time the needle is updated in the existing design, the entire gauge has to be re-drawn. This is not uncommon but just felt wrong to me. To tackle this I had to understand LAYERS. It turns out that if you make the position of CANVASSES absolute, you can put one on top of another (you also need to understand zIndex but that’s easy). If you can do that, then you can draw the gauge backdrop including the text and pretty-coloured segments once only on the bottom layer and only then clear and update the top layer, say, 50 times a second with needle updates etc. That saves some calculating.

But now I needed two CANVAS items which looks a little odd when setting up so I worked out how to create both, totally in Javascript. That led to another problem, in Node-Red you can’t just put your CANVAS elements in the top left corner of the page BODY – it won’t work. I then reasoned that both CANVASSES could sit inside a DIV on top of each others – and indeed they can. So now all that is needed in HTML is to define a DIV and the Javascript will do the rest.

That still left me with a mass of messy Javascript - and the thought of explaining what was and what was not relevant to end users in the blog here was keeping me awake. That’s when I started looking into this whole object encapsulation thing. 2 days ago that was a distant world, something for the future. Today, I wonder why I waited so long to take the effort to learn. I had to learn some new tricks in the process – however, when you eventually see the new gauge, setting it up will be as easy as this.

var gauge1= new petesGauge;

  container: "myDiv",


Define a gauge, point it to a DIV at which point it appears magically – and then dynamically set a value which will slowly change in front of your eyes. No other information exposed to the outside world, no need to do anything other than set up an empty DIV and add a link to the Javascript compressed library.

Part of the magic here is in the encapsulation I’ve learned to handle – but also of course there are many defaults. In the top section you’ll be able to override defaults and turns things on and off if you want – and in the bottom section there will be several settable items – the value of two gauge needles, the value of set points, visibility of LEDs etc. All easy to use options.

I’m on the right path – I understand what I’m doing but I’ve only so much time to code this up.

If anyone fancies themselves a whiz at JS Objects, I’ve fathomed out how to get and set global vars in the object with simple methods – no problem but can i hell interact externally with internal vars – happy to have a chat with anyone interested in enlightening me.

jsFiddle helped me get this far but the best tool for the job IMHO apart from lack of formatting – is CodePen – you can make code changes dynamically and see changes in real time – what a difference this makes. Oh and I realised the one thing missing from my LCD display (not shown above) is the overspill of side lighting typically seen on real LCD displays. That’s coming soon.

New Toys: I’ve also discovered (thanks to MrShark) Atom this week which promises to be a great editor both in Windows and on my little SBCs.  My MINT installation, apart from falling to bits visually after a “sleep” operation, continues to function well on the laptop and I’ve received some nice new DIN rail brackets from China which can screw onto the back of a perspex sheet for mounting boards and supplies. More on all of this later. Incidentally for those wondering, my testbed wireless Orange Pi Zero and twin NanoPi M3 units are still sitting on the bench, still working perfectly despite disconnecting the WIFI several times deliberately.

Desktop Touch Controller: And on another subject, how is my Desktop Pi Touch Controller doing? For some time Desktop Controllernow I’ve had a 5” touch screen attached to a Raspberry Pi 2, using Node-Red Dashboard as a simple controller for my office – to turn lights on and off and keep an eye on temperatures etc.  Works a treat using Chromium in “kiosk mode” – but two things have annoyed me – one being that menu from Node-Red Dashboard – the second being the ability of the browser to fail occasionally due to connection issues and put up a stupid message instead of trying to reconnect.  Well, I may have cracked both of these as for the last few days the little unit has been sitting there rock-solidly doing it’s job.

The annoying Node-Red-Dashboard unwanted menu problem (well I only have one page on this unit – why do I want a menu) was solved simply by a reader who wrote in to suggest this added to the style.

#toolbar {display:none;}

Yes, that’s it – simple as that. Gone – space retrieved.

The second came from here. http://www.labs.bristolmuseums.org.uk/running-google-chrome-in-kiosk-mode-tips-tricks-and-workarounds/

So I have a couple of templates on the page – one at the top with my CSS and some useful Javascript – the second at the bottom.

I’ve added this to the top

<body onClick=”location.reload()”>

and this to the bottom.

<script type=”text/javascript”> $(function() { setTimeout(function () { if($(“#VisitorStoriesHelpText”).length>0){ $(‘body’).attr(“onClick”,””) } }, 1 * 1 * 1000);}); </script>

And yes, I know that body code is really not appropriate in a Node-Red template – but guess what… it seems to work. I’ve not had an issue with the display for days now. May be a fluke of course – but fingers crossed.


What, ANOTHER Gauge?

Now, before you say anything – this gauge is different to the rest.. It is all mine and it EVOLVING (last update Feb 03) if you’ve seen this blog entry before today…

Gauges: Peter Scargill's GaugeI’ve spent a lot of time ploughing through code, some of it years old (SteelSeries) and though many of these designs are beautiful, most are not that well supported if at all and some comprise libraries that are kitchen sink jobs that take some learning, so much so that recently, in a fit of peak, after failing to figure out how to add a simple second dial or eliminate the wasted space of that metal outer ring in SteelSeries (it is easy to make it disappear but not the space it occupies) that I decided to “give it a go” myself.

What you see here is not finished but it works (and is a lot prettier when you see it running).  It scales without internal adjustments other than the gauge size as I need something that will easily adapt to differing Node-Red template blocks.

Images: There are several images on my site – don’t use them permanently please – grab them for yourself – they likely will change anyway.

As the dials move below the (circular) set-points the LEDS will go on and off – one triggers BELOW the set-point (heat), the other ABOVE (dehumidifier).

In the process of making this I’ve made a number of dials and centres – all in PowerPoint – very simple.

Centre 1 by Peter ScargillCentre 2 by Peter ScargillCentre 2 by Peter Scargill

I call these three metalCarvedCentre.png, greenCarveCentre.png and greyCentre.png respectively. I don’t plan to make a lot as there must be millions of needles and centres out there already.

tmpFF04Glowing rings are easy to add… and I’m working on an LCD display background image that’s hopefully a little more convincing than the canvas-manufactured version I’m using right now.

Glowing rings by Peter Scargill - in PowerPoint

If you poke the test values, you should see the gauge needles move smoothly to their destination. So I’m really happy about (and this happens a LOT it seems) having to update the whole thing every 20ms for animation when all I want is to update the dials (and an LCD panel eventually) – but I can’t find LAYERS anywhere.

What I HAVE done elsewhere is use Z-laying with multiple canvases to achieve layering as you will see in the link below. Originally I had 3 layers – the background, the bits that change and the knob over the needle – all of which SHOULD have worked perfectly – but for reasons beyond me the knob didn’t always show. I moved it in with the dial and all is well – so only two identical canvas units are needed. This seems like a good general way forward. I will change my main dial code to follow this pattern eventually when I've pondered what effect this will have on interfacing.  Anyway, I put this together from scratch and it seems to work ok, Take a look at this.. https://jsfiddle.net/scargill/r4qstas2/6/


A timer runs constantly but does nothing unless a change is made  and the values are then incremented or decremented until they  match the required values.

I think this will make a fine addition to the visual tools we have but still needs much tidying up so no comments about code quality please.

Fonts: Fonts are always a thing – and the normal web ones are today considered to be utterly pants. So – TTF fonts are good but also large – so I found this online convertor. You simply drag a TTF file of your choosing (clearly if for commercial purposes, you need to make sure it is free) and you get the option to download from a link – it works. So I wanted a nice dot matrix font for the little display here – and I found “Dots all for Now JL” which is pretty but also 44K. I ran it through the convertor and in less than a minute I was up and running with a font that is only 7k.

To use the font, I did this..

<style type="text/css">
@font-face {
font-family: "DOTMAT";
src: url("/myfonts/dotsalfn.woff") format('woff');

and then just referred to DOTMAT as you would any font. Now there are all sorts of warnings on the web about ensuring fonts are loaded first and working with CANVAS etc. – but somehow, in Node-Red, it all just works… which is nice and keeps the code simple.

And that’s it for now. I’m working on improving the code.

[{"id":"22bad52a.82347a","type":"ui_template","z":"c552e8d2.712b48","group":"40cf30b4.d9549","name":"MyGauge","order":0,"width":"6","height":"6","format":"<style>\n  @font-face {\n    font-family: \"DOTMAT\";\n    src: url(\"/myfonts/dotsalfn.woff\") format('woff');\n  } \n</style>\n\n<script>\n  var showNeedle2 = true;\nvar showLED1 = true;\nvar showLED2 = true;\nvar needleWidth1 = 1;\nvar needleWidth2 = 1;\n\nvar set1 = 0;\nvar set2 = 0;\nvar value1 = 0;\nvar value2 = 0;\n\nvar setpoint1 = -1;\nvar setpoint2 = -1;\nvar degrees = -1;\nvar degrees2 = -1;\n\nvar title = \"Pete's Aircon\";\nvar subTitle = \"Hmm1\";\nvar ledTitle1 = \"DEHUM\";\nvar ledTitle2 = \"HEATING\";\n\nvar needle1 = new Image();\nvar needle2 = new Image();\nvar centre = new Image();\n\n\n\nvar direction1 = 1;\nvar direction2 = 1;\n\n(function(scope) {\n  scope.$watch('msg', function(msg) {\n    if (typeof(msg.value1) != \"undefined\") value1 = msg.value1;\n    if (typeof(msg.value2) != \"undefined\") value2 = msg.value2;\n    if (typeof(msg.set1) != \"undefined\") set1 = msg.set1;\n    if (typeof(msg.set2) != \"undefined\") set2 = msg.set2;\n  });\n})(scope);\n\n\n\n\nfunction n(n) {\n  return n > 9 ? \"\" + n : \"0\" + n;\n}\n\n\nfunction init() {\n  needle1.src = \"http://www.scargill.net/things/needles/redCurvedNeedle.png\";\n  needle2.src = \"http://www.scargill.net/things/needles/greenCurvedNeedle.png\";\n  centre.src = \"http://www.scargill.net/things/needles/greyCarvedCentre.png\";\n  canvas = document.getElementById(\"fred\");\n  ctx = canvas.getContext(\"2d\");\n  cX = Math.floor(canvas.width / 2);\n  cY = Math.floor(canvas.height / 2);\n  dX = cX / 175; // divisor for centrepiece sizing\n  dY = cY / 175;\n\n  setInterval(draw, 50);\n\n}\n\n// draw a wedge\nfunction drawWedge(percent, color, count) {\n  var arcRadians = ((percent / 100) * 360) * (Math.PI / 180),\n    startAngle = totalArc,\n    endAngle = totalArc - arcRadians;\n  ctx.save();\n  ctx.beginPath();\n  ctx.moveTo(cX, cY);\n  ctx.arc(cX, cY, radius_outer, startAngle, endAngle, true);\n  /** cut out the inner section by going in the opposite direction **/\n  ctx.fillStyle = color;\n  ctx.arc(cX, cY, radius_inner, endAngle, startAngle, false);\n  ctx.closePath();\n  ctx.fill()\n  ctx.restore();\n  totalArc -= arcRadians;\n}\n\nfunction drawbit(i, colr) {\n  if (i & 1) drawWedge(1.1, colr, i);\n  else drawWedge(0.2, \"#cccccc\", i);\n}\n\n// draw the donut one wedge at a time\nfunction drawDonut() {\n  var r, g, b;\n  b = 0;\n  g = 0;\n  r = 255;\n  for (var i = 0; i < 100; i++) {\n    var r, g, b;\n    if (i < 40) {\n      g += 8;\n    }\n    if ((i > 40) && (i < 70)) {\n      g -= 8;\n      r -= 12;\n    }\n    if (i > 70) {\n      g -= 8;\n      b += 12;\n    }\n    drawbit(i, \"rgba(\" + r + \",\" + g + \",\" + b + \",1)\");\n  }\n}\n\nfunction roundRect(ctx, x, y, width, height, radius, fill, stroke) {\n  if (typeof stroke == 'undefined') {\n    stroke = true;\n  }\n  if (typeof radius === 'undefined') {\n    radius = 5;\n  }\n  if (typeof radius === 'number') {\n    radius = {\n      tl: radius,\n      tr: radius,\n      br: radius,\n      bl: radius\n    };\n  } else {\n    var defaultRadius = {\n      tl: 0,\n      tr: 0,\n      br: 0,\n      bl: 0\n    };\n    for (var side in defaultRadius) {\n      radius[side] = radius[side] || defaultRadius[side];\n    }\n  }\n  ctx.beginPath();\n  ctx.moveTo(x + radius.tl, y);\n  ctx.lineTo(x + width - radius.tr, y);\n  ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);\n  ctx.lineTo(x + width, y + height - radius.br);\n  ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);\n  ctx.lineTo(x + radius.bl, y + height);\n  ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);\n  ctx.lineTo(x, y + radius.tl);\n  ctx.quadraticCurveTo(x, y, x + radius.tl, y);\n  ctx.closePath();\n  if (fill) {\n    ctx.fill();\n  }\n  if (stroke) {\n    ctx.stroke();\n  }\n}\n\nfunction drawCircle() {\n  ctx.save();\n  /** outer ring **/\n  ctx.beginPath();\n  ctx.moveTo(cX, cY);\n  ctx.shadowBlur = 5 * dX;\n  ctx.shadowColor = \"rgba(40,40,40,1)\";\n  ctx.arc(cX, cY, radius + (8 * dX), 0, 2 * Math.PI, false);\n  ctx.arc(cX, cY, radius + (6 * dX), 0, 2 * Math.PI, true);\n  ctx.closePath();\n  ctx.fillStyle = \"rgba(40,40,40,1)\";\n  ctx.fill();\n  ctx.restore();\n  \n  \n  // do an arc of numbers...\n   ctx.save();\n    ctx.translate(canvas.width / 2, canvas.height / 2);\n    ctx.rotate(-140 * (Math.PI / 180));\n    ctx.font = \"bold \" + String(Math.floor(cX / 13)) + \"px Helvetica\";\n    ctx.textAlign = 'center';\n    ctx.fillStyle = '#000000';\n    for (var a=0;a<=100; a+=10)\n    {\n    ctx.rotate(23 * (Math.PI / 180));\n    ctx.fillText(n(a), 0, -(cY*0.65)); \n    }\n    ctx.restore();\n  \n\n  ctx.save();\n\n  if (showLED1 == true) {\n    /** Sub label 1 **/\n    ctx.font = \"bold \" + String(Math.floor(cX / 14)) + \"px Helvetica\";\n\n    // ctx.font = \"bold \" + String(Math.floor(cX / 14)) + \"px Helvetica\";\n    ctx.textAlign = 'center';\n    ctx.fillStyle = '#8A8A8A';\n    ctx.fillText(ledTitle2, cX + (cX / 2.5), cY - (cY / 9), (cX / 1));\n    if (degrees < setpoint2) {\n      ctx.beginPath(); // red led - size and scale need to be related to canvas - currently fixed\n      ctx.arc(cX + (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n      ctx.closePath();\n      ctx.shadowBlur = 20;\n      ctx.shadowColor = \"rgba(255,0,0,1)\";\n      ctx.fillStyle = \"rgba(255,0,0,1)\";\n      ctx.fill();\n      ctx.restore();\n    } else {\n      ctx.beginPath(); // red led - size and scale need to be related to canvas - currently fixed\n      ctx.arc(cX + (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n      ctx.closePath();\n      ctx.fillStyle = \"rgba(140,40,40,1)\";\n      ctx.fill();\n      ctx.lineWidth = 2 * dX;\n      ctx.strokeStyle = 'rgba(40,0,0,0.6)';\n      ctx.stroke();\n      ctx.restore();\n    }\n  }\n  if (showLED2 == true) {\n    /** Sub label 1 **/\n    ctx.font = \"bold \" + String(Math.floor(cX / 14)) + \"px Helvetica\";\n    ctx.textAlign = 'center';\n    ctx.fillStyle = '#8A8A8A';\n    ctx.fillText(ledTitle1, cX - (cX / 2.5), cY - (cY / 9), (cX / 1));\n\n    if (degrees2 > setpoint1) {\n      ctx.save(); // green led - size and scale need to be related to canvas - currently fixed\n      ctx.beginPath();\n      ctx.arc(cX - (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n      ctx.closePath();\n      ctx.shadowBlur = 20;\n      ctx.shadowColor = \"rgba(0,200,0,1)\";\n      ctx.fillStyle = \"rgba(0,200,0,1)\";\n      ctx.fill();\n      ctx.restore();\n    } else {\n      ctx.save(); // green led - size and scale need to be related to canvas - currently fixed\n      ctx.beginPath();\n      ctx.arc(cX - (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n      ctx.closePath();\n      ctx.fillStyle = \"rgba(40,110,40,1)\";\n      ctx.fill();\n      ctx.lineWidth = 2 * dX;\n      ctx.strokeStyle = 'rgba(0,40,0,0.6)';\n      ctx.stroke();\n      ctx.restore();\n    }\n  }\n\n  /** Main label **/\n  ctx.save();\n\n  ctx.beginPath;\n  roundRect(ctx, cX - (cX / 1.7), cY + (cY / 3), cX + (cX / 5.1), cY - (cY / 1.5), dX * 10, true);\n  ctx.clip()\n\n  ctx.beginPath;\n  ctx.strokeStyle = 'black';\n  ctx.lineWidth = 5;\n  ctx.shadowBlur = 15;\n  ctx.shadowColor = 'black';\n  ctx.shadowOffsetX = 0;\n  ctx.shadowOffsetY = 0;\n  ctx.fillStyle = \"rgba(255, 255, 180, .8)\";\n  roundRect(ctx, cX - (cX / 1.7) - 4, cY + (cY / 3) - 4, cX + (cX / 5.1) + 8, cY - (cY / 1.5) + 8, dX * 10, true);\n\n  ctx.restore();\n  ctx.save();\n\n  ctx.font = \"bold \" + String(Math.floor(cX / 8)) + \"px DOTMAT\";\n  ctx.textAlign = 'center';\n  ctx.fillStyle = '#8A8A8A';\n  ctx.fillText(title, cX, cY + (cY / 2.05));\n\n  /** Sub label **/\n  ctx.font = \"bold \" + String(Math.floor(cX / 12)) + \"px Helvetica\";\n  ctx.textAlign = 'center';\n  ctx.fillStyle = '#8A8A8A';\n  ctx.fillText(subTitle, cX, cY + (cY / 1.65));\n  ctx.restore();\n}\n\nfunction draw() {\n\n  if ((set1 == setpoint1) && (set2 == setpoint2) && (value1 == degrees) && (value2 == degrees2)) return;\n\n  if (set1 > setpoint1) setpoint1++;\n  else if (set1 < setpoint1) setpoint1--;\n  if (set2 > setpoint2) setpoint2++;\n  else if (set2 < setpoint2) setpoint2--;\n\n  if (value1 > degrees) degrees++;\n  else if (value1 < degrees) degrees--;\n  if (value2 > degrees2) degrees2++;\n  else if (value2 < degrees2) degrees2--;\n\n\n  width = 18 * dX,\n    radius = cX * .9,\n    radius_outer = cX * .9,\n    radius_inner = (radius - width) - (11 * dX),\n    kerning = 0.04,\n    color_alpha = 0.3;\n  totalArc = .47; // starting point for the arc\n\n  ctx.save();\n  // Radii of the white glow.\n  innerRadius = 20 * dX;\n  outerRadius = canvas.height / 2;\n  gradient = ctx.createRadialGradient(canvas.width / 2, canvas.height / 2, innerRadius, canvas.width / 2, canvas.height / 2, outerRadius);\n  gradient.addColorStop(0, 'white');\n  gradient.addColorStop(1, '#bbbbbb');\n  ctx.arc(canvas.width / 2, canvas.height / 2, canvas.height / 2 - (10 * dX), 0, 2 * Math.PI);\n  ctx.fillStyle = gradient;\n  ctx.fill();\n  ctx.restore();\n\n  drawCircle();\n  drawDonut();\n  \n  // Humid Circle\n  ctx.save();\n  ctx.beginPath();\n  ctx.translate(canvas.width / 2, canvas.height / 2);\n  tdegrees = -204 + (degrees2 * 227 / 100)\n    ctx.rotate((233 / 100 * setpoint1 - 118) * (Math.PI / 180));\n    ctx.beginPath();\n    ctx.arc(0, -(cY*0.82),cY*0.05, 0, 2 * Math.PI, false);\n    ctx.fillStyle = 'green';\n    ctx.fill();\n    ctx.lineWidth = 1.5;\n    ctx.strokeStyle = '#ffffff';\n    ctx.stroke();\n  ctx.restore();\n\n  // Temperature Circle\n  ctx.save(); \n  ctx.beginPath();\n  ctx.translate(canvas.width / 2, canvas.height / 2);\n  tdegrees = -204 + (degrees2 * 227 / 100)\n  ctx.rotate((233 / 100 * setpoint2 - 118) * (Math.PI / 180));\n  //ctx.beginPath();\n    ctx.arc(0, -(cY*0.82),cY*0.05, 0, 2 * Math.PI, false);\n    ctx.fillStyle = 'red';\n    ctx.fill();\n    ctx.lineWidth = 1.5;\n    ctx.strokeStyle = '#ffffff';\n    ctx.stroke();\n  ctx.restore();\n\n  // Save the current drawing state\n  ctx.save();\n  ctx.beginPath();\n  ctx.translate(canvas.width / 2, canvas.height / 2);\n  tdegrees = -204 + (degrees * 227 / 100)\n  ctx.rotate(tdegrees * (Math.PI / 180));\n\n  // shadow on lines??\n  ctx.shadowBlur = 4;\n  ctx.shadowColor = \"rgba(0,0,0,0.2)\";\n  ctx.shadowOffsetX = 5 * dX;\n  ctx.shadowOffsetY = 5 * dX;\n\n\n  ctx.drawImage(needle1, (canvas.width / 7) - (canvas.width / 4), -(canvas.height * needleWidth1 / 80), (canvas.height / 2), (canvas.width * needleWidth1 / 40));\n  // Restore the previous drawing state\n  ctx.restore();\n\n  if (showNeedle2 == true) {\n    // Save the current drawing state\n    ctx.save();\n    ctx.beginPath();\n    ctx.translate(canvas.width / 2, canvas.height / 2);\n    tdegrees = -204 + (degrees2 * 227 / 100)\n    ctx.rotate(tdegrees * (Math.PI / 180));\n    // shadow on lines??\n    ctx.shadowBlur = 4;\n    ctx.shadowColor = \"rgba(0,0,0,0.2)\";\n    ctx.shadowOffsetX = 5 * dX;\n    ctx.shadowOffsetY = 5 * dX;\n    ctx.drawImage(needle2, (canvas.width / 7) - (canvas.width / 4), -(canvas.height * needleWidth2 / 80), (canvas.height / 2), (canvas.width * needleWidth2 / 40));\n    ctx.restore();\n  }\n  ctx.save();\n  ctx.beginPath();\n  ctx.translate(canvas.width / 2, canvas.height / 2);\n  // draw the centre bit then restore the previous drawing state\n  ctx.drawImage(centre, (0 - (centre.height / 4)) * dX, (0 - (centre.width / 4)) * dY, (centre.height / 2) * dX, (centre.width / 2) * dY);\n  ctx.restore();\n  subTitle = \"Temp=\" + n(Math.floor(degrees)) + \"c \" + \"Hum=\" + n(Math.floor(degrees2)) + \"% \";\n}\n\ninit();\n\n</script>\n\n<canvas id = \"fred\"\nwidth = 310 height = 310 > </canvas>","storeOutMessages":true,"fwdInMessages":true,"x":980,"y":2480,"wires":[[]]},{"id":"fbecd5a1.989ac8","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"22","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2340,"wires":[["9f9e89fe.db3518"]]},{"id":"6e98eeac.504cf","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"35","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2380,"wires":[["9f9e89fe.db3518"]]},{"id":"9f9e89fe.db3518","type":"function","z":"c552e8d2.712b48","name":"msg.value1","func":"msg.value1=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":2360,"wires":[["22bad52a.82347a"]]},{"id":"10d0f056.a552e","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"11","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2420,"wires":[["9eb8ebf8.be3108"]]},{"id":"ae37427b.2f8b3","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"60","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2460,"wires":[["9eb8ebf8.be3108"]]},{"id":"9eb8ebf8.be3108","type":"function","z":"c552e8d2.712b48","name":"msg.value2","func":"msg.value2=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":2440,"wires":[["22bad52a.82347a"]]},{"id":"2251f312.3a575c","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"10","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2500,"wires":[["7c5ad2de.3c767c"]]},{"id":"ae3f6f46.2aed8","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"20","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2540,"wires":[["7c5ad2de.3c767c"]]},{"id":"7c5ad2de.3c767c","type":"function","z":"c552e8d2.712b48","name":"msg.set1","func":"msg.set1=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":780,"y":2520,"wires":[["22bad52a.82347a"]]},{"id":"4a2ac4f5.5906dc","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"30","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2580,"wires":[["f6ffef5f.562ca"]]},{"id":"8f5b3775.03c018","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"40","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2620,"wires":[["f6ffef5f.562ca"]]},{"id":"f6ffef5f.562ca","type":"function","z":"c552e8d2.712b48","name":"msg.set2","func":"msg.set2=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":780,"y":2600,"wires":[["22bad52a.82347a"]]},{"id":"40cf30b4.d9549","type":"ui_group","z":"","name":"testa","tab":"66a97521.af8dac","disp":true,"width":"6"},{"id":"66a97521.af8dac","type":"ui_tab","z":"","name":"testa","icon":"dashboard"}]

Sonoff 4CH 4 Channel Mains Control


Sonoff Sonoff 4CHA parcel turned up for me today – the Sonoff 4CH. You may recall I wrote some time ago about a smaller version they put out and I was quite scathing about the wiring. I turned out as you recall that this was a third party product and they were in the process of making their own version. Well, this is it.

The unit uses an ESP8285 (like the 8266 but with built-in Flash) to make a low cost 4-way mains power switch in a very nice DIN box. The price of the unit, at around £11.53 + postage, makes it cheaper than some people charge just for the box!!!

If you read this blog regularly you’ll know I’ve no interest in running their products on their own cloud – and that despite having my own very comprehensive software, this fellow has recently IMHO taken the lead in powering Sonoff products with his code. Well, it just so happens he supports this boar,  so this morning I grabbed the latest software, put in my MQTT and WIFI credentials, made the one change to the code needed to run this board (a single define) and off I went to open the board up.

Inside is the usual handy programming connector needing 3v3 and ground as well as serial in and out from an FTDI. As usual, I broke all the rules and instead of using a proper 3v3 supply I powered the board for the purposes of programming, directly from the FTDI – remembering to set it to 3v3 and not 5v.  At first I got nowhere until I realised I still had my Arduino environment set to ESP8266 – a quick check and there is indeed a setting for the smaller board.

And that was it – nothing to report really – it works as you’d expect.  Now what I don’t understand are the little covers – which on one side of the board cover up the 4 buttons to turn the outputs on and off (one of them is also the programming button) and on the other side the LEDs.  I will be drilling 5 holes in there so I can actually see the LEDs – but that’s just me.

Itead Sonoff 4CHDIN mounting, well put-together, inexpensive 4-relay switching.  The information claims you can use a maximum of 2.5Kw and that each output is 10 amps… well, I’m not sure I’d want to put 10 amps through there especially inductive, maybe more like half of that, while obeying the total of no more than 2.5KW.  There’s a little fuse on the board on the incoming live - but to save opening it up in the event of problems and finding a tiny fuse, I’d be thinking about the lowest sensible value of fuse in your power lead. I put a 10 amp fuse in there and may yet replace it with a 5 amp (of course what this means depends on which side of the pond you are on – we Brits get a lot more power out of 10 amps than our American friends).

You may notice something odd  - or depending where you live you might not find it odd at all – the neutral block is green and the earth block is grey. You see, being a Brit – I associate earth with grass – i.e. green and our earth wire is either green or some combination of green and yellow… so I could see someone being caught out by that – you’ll notice my wiring looks off with the green earth heading into the grey box!!!   Those push connectors incidentally work a treat! With previous Sonoff products I had people asking me what to do with the Earth wire – no such problem here.

This one is going to Spain with me – we have Star wiring over there and spare room on the power DIN rail so this will fit in very nicely to control some lighting and one small heater over there. If you’re starting from scratch this could make a decent low-cost-per-relay move into home control along with whatever software you use to control things – in my case – I send commands out from Node-Red via MQTT straight to the board over WIFI.


ITead Sonoff 4CH circuit board


At least on the surface, another winner from this company. Don’t all rush as Itead are on Chinese holiday until 3rd of Feb!


The Mint Experiment

Anyone who’s known me for years knows I’m a died in the wool Windows man.  Over many years from Windows 3.1 onwards, I’ve done down that road until ultimately all my machines are now Windows 10 – and I have to say that, while it could be argued before Windows 7 that it was not the most reliable operating system in the world, from that point on pretty much all of that changed. I regularly leave Windows 10 machines running for weeks on end and I’m sure they’d keep going for many months if it were not for the only remaining issue- that of pesky updates which Microsoft are determined we have whether we like it or not.

Now to be fair there was a time when Adobe – a company I cannot stand, used to issue updates for Acrobat almost on a daily basis and at least that no longer happens. I can usually tell when Windows wants me to update because Skype conversations become almost unusable and other strange things happen – at that point I reboot the machine and lo and behold – a Windows update is in progress – don’t turn off your machine. Thanks – I’m in a hurry for  train…

In my previous role as IT Director of the FSB, I would take it upon myself, being a hands-on type, from time to time, to try the latest Linux on one of our PCs, only to end up with utter disappointment as it would fail to connect to a WIFI access point or the video would hang over the end or some such issue – there was always SOMETHING – and so I would scrap that idea for a several months before trying again. At one point I used to get hate mail from  members who were clearly selling Linux machines -  for supporting Microsoft! Serious hate mail.  So over the years I kept trying again and always ending up with disappointment. (I’ve never used Linux on my personal machines because yes, I do like the latest state of the art, graphically intensive games and yes I do use lots of proprietary packages such as Magix and others which are simply not available on Linux.

In recent times as regular readers know, I’ve been forced into taking an interest in Linux because Debian (a Linux variation) runs on the Raspberry Pi. 2 years ago I bought a Raspberry Pi 2 (having played with the original Pi, loaded up the graphical operating system and immediately put it on Ebay in disgust at the speed). On the Pi2, I was pleasantly surprised to see that Raspbian ran at a reasonable speed and since then I’ve done many different board reviews and installed Raspbian and Debian on lots of boards, leaning on experts along the way as my knowledge started to build.

A while back I took the plunge and installed Ubuntu onto one of these boards and with help from others soon came to realise that there were not THAT many differences between Debian and Ubuntu and one of the things that has struck me in all this time is how reliable the operating system can be – I’ve a Pi that’s been sitting controlling stuff for well over a year now without as much as a sneeze despite me poking live updated and tweaks into it without rebooting.

Linux Mint on an old Dell laptop

And so with that in mind, last week I took an old DELL E4300 I had lying around which had simply refused to update to Windows 10 from Windows 7 (no matter how many ways I came at it) and which was so old it was really not worth opening up – and grabbed myself a USB stick with Ubuntu on it, ready after maybe 3 years of abstinence and armed with much better knowledge than previously, to try again.

Well, what a disappointment that was. Ubuntu loaded up no problem, with it’s rather dated looking purple interface – and asked me for my WIFI password – I promptly gave it this – and before long I had a working laptop. Or so I thought. The WIFI icon looked broken – yet I could pull up a browser and go on the web  - no problems. I was impressed by the ability to watch video on the BBC website, something that in the past on Linux was just not on.

tmp95E7That enthusiasm lasted maybe an hour. The App store decided not to work – coming up blank. But hey, that was just one program. I noted a nice graphical email client complete with calendar. I set it up and within minutes I had my Google calendar up and running. But as soon as I tried putting in email  - “Cannot get email as there is no Internet connection”. I opened a browser and sure enough the Internet was fine – but still that broken WIFI indicator. From there, things went downhill – it could not store draft emails due to a permissions issue and – nope – sorry life is TOO SHORT FOR THIS – I was reminded of the frustration of previous years… what HAVE these Linux guys being doing all this time, I thought.

I was in the process of giving up when I read something about Linux MINT. I liked the interface. In the instructions for installation I had to go get PendriveLinux so I could install the image on a USB stick. You should be seeing links here as appropriate. I went off to the official download page and picked the 64 bit version using the Xfce graphical interface as it had been suggested that while simple out of the box, this version had lots of options. I put the Mint Linux onto the USB stick and put it into the laptop. I must admit I found a certain satisfaction in wiping Ubuntu. The installation went well and WIFI came up but this time, no broken WIFI indicator. The taskbar seemed to be missing a battery indicator but It didn’t take me long to figure out how to add all sorts of widgets to the taskbar to make me feel at home (including a battery indicator).

I noted that Thunderbird email was installed and I set that up with my two email accounts – no problem whatsoever… but no calendar. Of course, that’s a plug in and it needs another plug-in to get Google calendar functionality – but all of that took mere moments to organise and now I have fully fledged email and calendar. Granted it is a little slow at pulling in the 35,000 emails in my main in-box – but it’ll get there I’m sure.

I hit a few obstacles on the way - my by now standard VNC server would not have it until Mr Shark suggested I try  x11VNC – that worked a treat.  Then I had it asking pesky password questions every time I tried to breath – that was easy to fix  - then I noted on power up that the KEYRING wanted another password – you’d think I was operating a bank. That went quickly – and from there everything went smoothly.  But this was Linux MINT – about which I know nothing at all. The funny thing was, doing an APT-GET UPDATE showed that in fact this is Ubuntu Xenial… now I’d already, with lots of help from MrShark, modified my all-singing install script for Ubuntu. I didn’t really expect it to work on this machine but having written down the steps to put everything together I thought “what the hell” and ran the script – it failed of course as it looks for UBUNU, DEBIAN, RASPBIAN or DIETPI – and this was LINUXMINT. I added a check for the latter THOROUGHLY expecting a host of horrific error and compatibility errors.

I was with some delight that I returned 15 minutes later to find that not only had the script worked – but without a single error – adding NodeJS, Node-Red, Apache, PHP 7, Mosquitto, SQLITE, MC and several other programs to my installation.  I rebooted to ensure I wasn’t dreaming – sure enough – everything worked.

The laptop has no Bluetooth interface so I plugged in one of those cheap Chinese Bluetooth USB units… and went off to the Bluetooth controls – without ANY hassle my Bluetooth mouse connected!!! I plugged in my Bluetooth headset – it got that – I went off to the BBC website and…news.bbc.co.uk – the Bluetooth headset didn’t connect automatically – so I went to the volume control – it was in the options – sure enough – perfectly synced Bluetooth.

Now if SKYPE video will just work….

Up to now, hours later, I have a nicely usable laptop with all my development toys (well maybe not NotePad++ but there are a couple of decent Linux editors,  my email and calendar, Chrome browser and a full office suite – making an otherwise pretty hopeless old laptop into a useable tool!

Issues: This morning I ran out of battery. A sign came up to say the battery is low – save your work – but that that point the mouse and keyboard stopped working – hence saving work was impossible. A moment later, the laptop shut down. I charged it and it came straight back up with the same message – I had no option but to shut it down (sluggish mouse response as an aside). After rebooting all was well.   Also Skype does not appear to survive power cycling and has to be loaded again.

If you like this post – please share a link to it by social media, by email with friends or on your website.
More readers means more feedback means more answers for all of us. Thank you!


HTML Canvas Touch Position

tmp2D9EI’ve had some great fun the last few days working on my gauge – and as part of that I thought at one time of making it touch-sensitive. Thankfully my senses prevailed in the end but not before coming to terms with a need to understand how the HTML5 Canvas works.

If you’ve not played with this I highly recommend it. Great fun and just SO, SO very much better than computer-destroying FLASH.

Of course, many developers are well familiar with Canvas - but I wasn't - and yet it is essentially easy.  So I thought I'd jot down mouse stuff in case I need it in future - or someone else needs it.

So what you see upper right (just an image in this case) is a canvas area.  You can make an empty HTML page with nothing more than this in it..

#myCanvas {
background-color: rgba(158, 167, 184, 0.2);

<canvas id="myCanvas" width=350 height=350></canvas>

The first half is optional and is just to give the script a background colour so you can SEE it !!!

The second half defines a canvas area 350px square with the name (id) “myCanvas” – call it anything you like. Armed with that, in my gauge I have a nice curved set of blocks indicating 0-100 degrees.  I read up how to get the mouse position on "mouse up" – easy enough  - in a call-back function– you’ll see that in the jsFiddle code link below – you can play with this to your heart’s content in a browser.

My problem was – how do I convert that X/Y coordinate pair into something that tells me which part of the temperature curve I was in when I clicked the mouse!!! At that point I almost recoiled into my beer so I rang up my pal Peter Oakes in Canada who reminded me that 50 years ago at school I did algebra – and they rammed the sine rule into us.  It didn’t take long for it all to come back.  Reference the CENTRE of that canvas, any position is in one of 4 squares… and so depending which square the mouse is in, you can draw a right angled triangle from the mouse back to the centre and up (or down) to it’s X position.  Using a simple calculation you can then determine the angle up to 90 degrees. Again depending which of the four squares you are in – that totals up to 360 degrees – i.e. a circle. A little division as my gauges typically don’t use a complete circle and I end up with a value 0-100.

As it happens I didn’t need this but I’m sure anyone experimenting for the first time with Canvas interaction will find it useful – so here it is. Click in the area in the demo and you’ll see the angle and the X,Y coordinates of the mouse.

I’ve also put in the distance of the mouse from he centre. Using that, you could then define a narrow curved band in which you are interested in accepting values – or not.

IPossible use in a colour wheeln this case “touch” and “mouse” are interchangeable though clearly you can’t “hover” with a finger.

And just for good measure I added a pretty text gradient.

If you don't want to wait for mouse-button-pressing, change the reference in the code from mouseup to mousemove and press "run" - now you will get a continuous readout as long as the mouse is over the canvas area.

Let me in parting say this – my first few days of using Canvas were gruelling – and interacting with Node-Red even worse. With help from various people, all of that melted away and in my blog entries here you’ll see how to do the interaction. Now, I’m ready to go with Canvas, if you look at the last blog entry you’ll see I’ve tackled lots of things including text at arbitrary angled positions, arcs, inward shadows… slowly but surely it all comes together if you drink enough coffee.


It just occurred to me that if you wanted a colour control – one of those colour wheels, then the ANGLE would feed your colour wheel lookup… and the distance from the centre, the saturation with white at the centre!!



Thermometers for Node-Red

Three thermometers in a row for Node-Red (or as many or few as you like really) for Node-Red Dashboard. Another fine example of simple gauges and unlike some it is easy to make multiple gauges on one page – I will demonstrate three.

Here is the library source, I grabbed the larger one and saved as gauges-min.js in my /myjs folder – see previous blogs for setting this up with Node-Red.

Here is the code for one – and a flow example with three independent units.

You can of course alter the colours to whatever you want.

thermometer flow for Node-Red

The code for one template:

<script src="/myjs/gauge.min.js"></script>

            scope.$watch('msg', function(msg) {
               if (typeof(msg.payload) != "undefined") { gauge2.value=msg.payload; gauge2.draw(); }

var gauge2 = new LinearGauge({
    renderTo: 'mycanvas',
    valueBox: false,
    highlights: [ 
            {"from": 0, "to": 10, "color": "rgba(50, 50, 200, .75)"},
            {"from": 10, "to": 20, "color": "rgba(50, 200, 200, .75)"},
            {"from": 20, "to": 30, "color": "rgba(50, 200, 50, .75)"},
            {"from": 30, "to": 50, "color": "rgba(200, 200, 50, .75)"},
            {"from": 50, "to": 100, "color": "rgba(200, 50, 50, .75)"}
    barWidth: 10,
    units: "°C",
    borderShadowWidth: 0,
    borders: false,
    value: 35


<canvas id="mycanvas" 

The sample flow above

[{"id":"ddc888ba.e45578","type":"ui_template","z":"c552e8d2.712b48","group":"33279d5b.72b122","name":"controller","order":0,"width":"2","height":"5","format":"<script src=\"/myjs/gauge.min.js\"></script>\n\n<script>\n  (function(scope){ \n            scope.$watch('msg', function(msg) {\n               if (typeof(msg.payload) != \"undefined\") { gauge3.value=msg.payload; gauge3.draw(); }\n            });\n    })(scope);\n\nvar gauge3 = new LinearGauge({\n    renderTo: 'mycanvas2',\n    valueBox: false,\n    highlights: [ \n            {\"from\": 0, \"to\": 10, \"color\": \"rgba(50, 50, 200, .75)\"},\n            {\"from\": 10, \"to\": 20, \"color\": \"rgba(50, 200, 200, .75)\"},\n            {\"from\": 20, \"to\": 30, \"color\": \"rgba(50, 200, 50, .75)\"},\n            {\"from\": 30, \"to\": 50, \"color\": \"rgba(200, 200, 50, .75)\"},\n            {\"from\": 50, \"to\": 100, \"color\": \"rgba(200, 50, 50, .75)\"}\n            ],\n    barWidth: 10,\n    units: \"°C\",\n    borderShadowWidth: 0,\n    borders: false,\n    value: 35\n}).draw();\n\n</script>\n\n<canvas id=\"mycanvas2\" \n    data-type=\"linear-gauge\"\n    data-width=\"106\"\n    data-height=\"270\"\n    data-units=\"°C\"\n    data-min-value=\"0\"\n    data-start-angle=\"90\"\n    data-ticks-angle=\"180\"\n    data-value-box=\"false\"\n    data-max-value=\"220\"\n    data-major-ticks=\"0,20,40,60,80,100,120,140,160,180,200,220\"\n    data-minor-ticks=\"2\"\n    data-stroke-ticks=\"true\"\n    data-color-plate=\"#fff\"\n    data-border-shadow-width=\"0\"\n    data-borders=\"false\"\n    data-needle-type=\"arrow\"\n    data-needle-width=\"2\"\n    data-needle-circle-size=\"7\"\n    data-needle-circle-outer=\"true\"\n    data-needle-circle-inner=\"false\"\n    data-animation-duration=\"1500\"\n    data-animation-rule=\"linear\"\n    data-bar-width=\"10\"\n    data-value=\"35\"\n></canvas>\n","storeOutMessages":false,"fwdInMessages":false,"x":960,"y":2120,"wires":[[]]},{"id":"27cb8298.0e94de","type":"ui_template","z":"c552e8d2.712b48","group":"33279d5b.72b122","name":"controller","order":0,"width":"2","height":"5","format":"<script src=\"/myjs/gauge.min.js\"></script>\n\n<script>\n  (function(scope){ \n            scope.$watch('msg', function(msg) {\n               if (typeof(msg.payload) != \"undefined\") { gauge4.value=msg.payload; gauge4.draw(); }\n            });\n    })(scope);\n\nvar gauge4 = new LinearGauge({\n    renderTo: 'mycanvas3',\n    valueBox: false,\n    highlights: [ \n            {\"from\": 0, \"to\": 10, \"color\": \"rgba(50, 50, 200, .75)\"},\n            {\"from\": 10, \"to\": 20, \"color\": \"rgba(50, 200, 200, .75)\"},\n            {\"from\": 20, \"to\": 30, \"color\": \"rgba(50, 200, 50, .75)\"},\n            {\"from\": 30, \"to\": 50, \"color\": \"rgba(200, 200, 50, .75)\"},\n            {\"from\": 50, \"to\": 100, \"color\": \"rgba(200, 50, 50, .75)\"}\n            ],\n    barWidth: 10,\n    units: \"°C\",\n    borderShadowWidth: 0,\n    borders: false,\n    value: 35\n}).draw();\n\n</script>\n\n<canvas id=\"mycanvas3\" \n    data-type=\"linear-gauge\"\n    data-width=\"106\"\n    data-height=\"270\"\n    data-units=\"°C\"\n    data-min-value=\"0\"\n    data-start-angle=\"90\"\n    data-ticks-angle=\"180\"\n    data-value-box=\"false\"\n    data-max-value=\"220\"\n    data-major-ticks=\"0,20,40,60,80,100,120,140,160,180,200,220\"\n    data-minor-ticks=\"2\"\n    data-stroke-ticks=\"true\"\n    data-color-plate=\"#fff\"\n    data-border-shadow-width=\"0\"\n    data-borders=\"false\"\n    data-needle-type=\"arrow\"\n    data-needle-width=\"2\"\n    data-needle-circle-size=\"7\"\n    data-needle-circle-outer=\"true\"\n    data-needle-circle-inner=\"false\"\n    data-animation-duration=\"1500\"\n    data-animation-rule=\"linear\"\n    data-bar-width=\"10\"\n    data-value=\"35\"\n></canvas>\n","storeOutMessages":false,"fwdInMessages":false,"x":960,"y":2200,"wires":[[]]},{"id":"7df32c36.100ef4","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"20","payloadType":"num","repeat":"","crontab":"","once":false,"x":770,"y":2100,"wires":[["ddc888ba.e45578"]]},{"id":"54305565.bdfd7c","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"60","payloadType":"num","repeat":"","crontab":"","once":false,"x":770,"y":2140,"wires":[["ddc888ba.e45578"]]},{"id":"7907996d.21af38","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"10","payloadType":"num","repeat":"","crontab":"","once":false,"x":770,"y":2180,"wires":[["27cb8298.0e94de"]]},{"id":"583f639c.99440c","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"90","payloadType":"num","repeat":"","crontab":"","once":false,"x":770,"y":2220,"wires":[["27cb8298.0e94de"]]},{"id":"c8fdd54c.823418","type":"ui_template","z":"c552e8d2.712b48","group":"33279d5b.72b122","name":"controller","order":0,"width":"2","height":"5","format":"<script src=\"/myjs/gauge.min.js\"></script>\n\n<script>\n  (function(scope){ \n            scope.$watch('msg', function(msg) {\n               if (typeof(msg.payload) != \"undefined\") { gauge2.value=msg.payload; gauge2.draw(); }\n            });\n    })(scope);\n\nvar gauge2 = new LinearGauge({\n    renderTo: 'mycanvas',\n    valueBox: false,\n    highlights: [ \n            {\"from\": 0, \"to\": 10, \"color\": \"rgba(50, 50, 200, .75)\"},\n            {\"from\": 10, \"to\": 20, \"color\": \"rgba(50, 200, 200, .75)\"},\n            {\"from\": 20, \"to\": 30, \"color\": \"rgba(50, 200, 50, .75)\"},\n            {\"from\": 30, \"to\": 50, \"color\": \"rgba(200, 200, 50, .75)\"},\n            {\"from\": 50, \"to\": 100, \"color\": \"rgba(200, 50, 50, .75)\"}\n            ],\n    barWidth: 10,\n    units: \"°C\",\n    borderShadowWidth: 0,\n    borders: false,\n    value: 35\n}).draw();\n\n</script>\n\n<canvas id=\"mycanvas\" \n    data-type=\"linear-gauge\"\n    data-width=\"106\"\n    data-height=\"270\"\n    data-units=\"°C\"\n    data-min-value=\"0\"\n    data-start-angle=\"90\"\n    data-ticks-angle=\"180\"\n    data-value-box=\"false\"\n    data-max-value=\"220\"\n    data-major-ticks=\"0,20,40,60,80,100,120,140,160,180,200,220\"\n    data-minor-ticks=\"2\"\n    data-stroke-ticks=\"true\"\n    data-color-plate=\"#fff\"\n    data-border-shadow-width=\"0\"\n    data-borders=\"false\"\n    data-needle-type=\"arrow\"\n    data-needle-width=\"2\"\n    data-needle-circle-size=\"7\"\n    data-needle-circle-outer=\"true\"\n    data-needle-circle-inner=\"false\"\n    data-animation-duration=\"1500\"\n    data-animation-rule=\"linear\"\n    data-bar-width=\"10\"\n    data-value=\"35\"\n></canvas>\n","storeOutMessages":false,"fwdInMessages":false,"x":960,"y":2040,"wires":[[]]},{"id":"8cc2ade9.5db9b","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"5","payloadType":"num","repeat":"","crontab":"","once":false,"x":770,"y":2020,"wires":[["c8fdd54c.823418"]]},{"id":"277f033f.43ecbc","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"48","payloadType":"num","repeat":"","crontab":"","once":false,"x":770,"y":2060,"wires":[["c8fdd54c.823418"]]},{"id":"33279d5b.72b122","type":"ui_group","z":"","name":"LCD Test","tab":"34cddaf3.8a9cd6","disp":true,"width":"6"},{"id":"34cddaf3.8a9cd6","type":"ui_tab","z":"","name":"testz","icon":"dashboard"}]

Yet Another Gauge

tmp6CBFThis one has been long in coming because despite my quickly evolving knowledge of Javascript and Canvas, that last leap – getting variable information out of a canvas and back into Node-Red – has kept me on edge for a couple of days now.  Dave (DCEEJAY) pointed me to various solutions all of which failed for me but then this morning he came up with a winner. I’ll go into detail in this article.

The gauge is simple enough – very much like the ones that Node-Red Dashboard already has – but it is adjustable (hmm, that’s a thought – I have suggested the Node-Red guys add a tick option to make the existing gauges adjustable. For now – this is it.

So – this started off as one of the examples from RGraph – you’ll need to include those libraries as previously. So note that the units used (degrees C) can be changed to anything you want – percentage or whatever. That is in the setup and is the entry marked “unitsPost”.

We have the by now familiar function(scope) function which allows us to inject values into the gauge – in this case msg.payload sets the gauge value.

We also have a special function which returns variable “value” to Node-Red on mouse up. This is the piece of the puzzle that has kept me going for days.  We also have a function which occurs on mouse up that grabs the current gauge value and stores it in “value”.  In the template itself, I’ve un-ticked “pass through messages” so that clicking or touching the gauge returns the new value of the gauge  - but injecting values does NOT product an output.

Now why is that last point important. Well, put such a gauge up on your computer screen and also on a secondary screen or phone – and watch what happens. Adjust one and…. nothing – the OTHER screen shows no change – oh dear. You need to inject the output back into the input for this to happen and unless you un-tick that box – the gauge will disappear up it’s own bum! For reasons beyond me you can’t connect a template’s output back to it’s input – so I just use an empty function to do that – well, empty other than I take the opportunity to put the value under the function just for effect.

I’ve made this to fit a 6*4 block. You can make it smaller by adjusting the size of the canvas, the gutter offsets and font size – but I could only manage to get one on a given tab without some unexplained interaction so I left the gauge this size which is handy for fingers on a typical phone.


A view of the code:

<script src="/myjs/RGraph/RGraph.common.core.js"></script>
<script src="/myjs/RGraph/RGraph.common.dynamic.js"></script>
<script src="/myjs/RGraph/RGraph.semicircularprogress.js"></script>

    var value=0;

        scope.$watch('msg', function(msg) {
               if (typeof(msg.payload) != "undefined") { ssp.value=msg.payload; ssp.grow(); }

this.scope.action = function() {
    return value;
    var ssp = new RGraph.SemiCircularProgress({
        id: 'newgauge1',
        min: 0,
        max: 100,
        value: 86,
        options: {
            gutterTop: 2,
            gutterLeft: 20,
            gutterRight: 5,
            gutterBottom: 70,
            unitsPost: '°C',
            labelsCenterSize: 40,
            labelsCenterValign: 'center',
            labelsMinSize: 10,
            labelsMaxSize: 10,
            adjustable: true,
            textAccessiblePointerevents: false,
            colors: ['Gradient(#224499:white)'],
            anglesStart: RGraph.PI - 0.5,
            anglesEnd: RGraph.TWOPI + 0.5
ssp.canvas.onmouseup = function (e)
    var obj   = e.target.__object__;

<canvas ng-mouseup="send({payload:action()})" class="knob" id="newgauge1" width="300" height="200">
    [No canvas support]

The actual flow for copying into Node-Red:

[{"id":"3cbe837a.faf1dc","type":"ui_template","z":"c552e8d2.712b48","group":"33279d5b.72b122","name":"controller","order":0,"width":"6","height":"4","format":"<script src=\"/myjs/RGraph/RGraph.common.core.js\"></script>\n<script src=\"/myjs/RGraph/RGraph.common.dynamic.js\"></script>\n<script src=\"/myjs/RGraph/RGraph.semicircularprogress.js\"></script>\n\n<script>\n    var value=0;\n\n    (function(scope){\n        scope.$watch('msg', function(msg) {\n               if (typeof(msg.payload) != \"undefined\") { ssp.value=msg.payload; ssp.grow(); }\n        });\n  \n})(scope);\n\nthis.scope.action = function() {\n    return value;\n}\n    \n    var ssp = new RGraph.SemiCircularProgress({\n        id: 'newgauge1',\n        min: 0,\n        max: 100,\n        value: 86,\n        options: {\n            gutterTop: 2,\n            gutterLeft: 20,\n            gutterRight: 5,\n            gutterBottom: 70,\n            unitsPost: '°C',\n            labelsCenterSize: 40,\n            labelsCenterValign: 'center',\n            labelsMinSize: 10,\n            labelsMinOffsetAngle:-0.1,\n            labelsMinOffsetx:0,\n            labelsMinOffsety:0,\n            labelsMaxSize: 10,\n            labelsMaxOffsetAngle:0.1,\n            labelsMaxOffsetx:0,\n            labelsMaxOffsety:0,\n            adjustable: true,\n            textAccessiblePointerevents: false,\n            colors: ['Gradient(#224499:white)'],\n            anglesStart: RGraph.PI - 0.5,\n            anglesEnd: RGraph.TWOPI + 0.5\n        }\n    }).grow();\n  \nssp.canvas.onmouseup = function (e)\n{\n    var obj   = e.target.__object__;\n    value=obj.value;\n}  \n </script>\n\n<canvas ng-mouseup=\"send({payload:action()})\" class=\"knob\" id=\"newgauge1\" width=\"300\" height=\"200\">\n    [No canvas support]\n</canvas>","storeOutMessages":true,"fwdInMessages":false,"x":940,"y":1880,"wires":[["5ea77ad6.1b8064","8dc0930c.a5aaf"]]},{"id":"ecf3d231.ddba3","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"40","payloadType":"str","repeat":"","crontab":"","once":false,"x":770,"y":1880,"wires":[["8dc0930c.a5aaf"]]},{"id":"d9c0b1d6.35dbb","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"80","payloadType":"str","repeat":"","crontab":"","once":false,"x":770,"y":1920,"wires":[["8dc0930c.a5aaf"]]},{"id":"5ea77ad6.1b8064","type":"debug","z":"c552e8d2.712b48","name":"","active":true,"console":"false","complete":"false","x":1130,"y":1880,"wires":[]},{"id":"8dc0930c.a5aaf","type":"function","z":"c552e8d2.712b48","name":"pass thru","func":"node.status({fill:\"blue\",shape:\"dot\",text:msg.payload});\n\nreturn msg;","outputs":1,"noerr":0,"x":940,"y":1940,"wires":[["3cbe837a.faf1dc"]]},{"id":"33279d5b.72b122","type":"ui_group","z":"","name":"LCD Test","tab":"34cddaf3.8a9cd6","disp":true,"width":"6"},{"id":"34cddaf3.8a9cd6","type":"ui_tab","z":"","name":"testz","icon":"dashboard"}]

Another LCD

LCD colours by Peter ScargillWhen writing that last blog entry about an LCD with time and date display and programmable temperature for Node-Red Dashboard, it occurred to me that it would be worthwhile making a simple one-liner (6*1) LCD with a 20-character display. At the same time I wanted to simplify it right down and here’s the result. Should make a good starting point for anyone wanting to develop their own.


In this case you simply fire a message at the template node. You only need one font for this from the ones used in the previous entry (really you should read the previous article to understand how to load the fonts in – trivial).

I’ve done a check on msg.payload and if the first character is a period I then use the remainder of the message to select colour – hence…


And that really is it – really simple to use – make the template 6*1,insert the code, make sure the font is in place and when you run it – you can select the sample text or play with the colours – enjoy.

LCD colours by Peter Scargill

LCD colours by Peter Scargill

LCD colours by Peter Scargill

[{"id":"7744e0e4.38986","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"Hello there","payloadType":"str","repeat":"","crontab":"","once":false,"x":800,"y":1180,"wires":[["864d97a5.30d348"]]},{"id":"543d4cba.38a7a4","type":"inject","z":"c552e8d2.712b48","name":"green","topic":"","payload":".blackOnGreen","payloadType":"str","repeat":"","crontab":"","once":false,"x":790,"y":1220,"wires":[["864d97a5.30d348"]]},{"id":"658a2f15.d95bc","type":"inject","z":"c552e8d2.712b48","name":"orange","topic":"","payload":".blackOnOrange","payloadType":"str","repeat":"","crontab":"","once":false,"x":790,"y":1260,"wires":[["864d97a5.30d348"]]},{"id":"b60e3a3f.931078","type":"inject","z":"c552e8d2.712b48","name":"blue","topic":"","payload":".blackOnBlue","payloadType":"str","repeat":"","crontab":"","once":false,"x":790,"y":1300,"wires":[["864d97a5.30d348"]]},{"id":"62767df.a99d984","type":"inject","z":"c552e8d2.712b48","name":"yellow","topic":"","payload":".blackOnYellow","payloadType":"str","repeat":"","crontab":"","once":false,"x":790,"y":1340,"wires":[["864d97a5.30d348"]]},{"id":"ad983ec0.bd2ed","type":"inject","z":"c552e8d2.712b48","name":"white","topic":"","payload":".blackOnWhite","payloadType":"str","repeat":"","crontab":"","once":false,"x":790,"y":1380,"wires":[["864d97a5.30d348"]]},{"id":"a046dc0e.953ea","type":"inject","z":"c552e8d2.712b48","name":"pink","topic":"","payload":".blackOnPink","payloadType":"str","repeat":"","crontab":"","once":false,"x":790,"y":1420,"wires":[["864d97a5.30d348"]]},{"id":"8372ff69.e0a4c","type":"inject","z":"c552e8d2.712b48","name":"red and light text","topic":"","payload":".yellowOnRed","payloadType":"str","repeat":"","crontab":"","once":false,"x":820,"y":1460,"wires":[["864d97a5.30d348"]]},{"id":"5fcd3b1d.a64954","type":"inject","z":"c552e8d2.712b48","name":"cyan and white","topic":"","payload":".whiteOnCyan","payloadType":"str","repeat":"","crontab":"","once":false,"x":820,"y":1500,"wires":[["864d97a5.30d348"]]},{"id":"a81aab9f.8f78a8","type":"inject","z":"c552e8d2.712b48","name":"lime on black","topic":"","payload":".limeOnBlack","payloadType":"str","repeat":"","crontab":"","once":false,"x":810,"y":1540,"wires":[["864d97a5.30d348"]]},{"id":"864d97a5.30d348","type":"ui_template","z":"c552e8d2.712b48","group":"33279d5b.72b122","name":"LCD","order":0,"width":"6","height":"1","format":"<script>\n    var LCDColours={\n    \"blackOnOrange\": {items:[\"#222\",\"#fb7c00\"]},    \n    \"blackOnGreen\" : {items:[\"#222\",\"#66ac66\"]},\n    \"blackOnBlue\" : {items:[\"#222\",\"#8888ff\"]},\n    \"blackOnYellow\" : {items:[\"#222\",\"#bbbb44\"]},\n    \"blackOnWhite\" : {items:[\"#222\",\"#aaaaaa\"]},\n    \"blackOnPink\" : {items:[\"#222\",\"#ff8888\"]},\n    \"yellowOnRed\" : {items:[\"#ccaa22\",\"#aa2222\"]},\n    \"whiteOnCyan\" : {items:[\"#dddddd\",\"#227777\"]},\n    \"orangeOnBlack\" : {items:[\"#ff8800\",\"#000000\"]},  \n    \"limeOnBlack\" : {items:[\"#00cc55\",\"#000000\"]} \n    } ;\n\n   (function(scope){ \n            scope.$watch('msg', function(msg) {\n                if (typeof(msg.payload) != \"undefined\") \n                        {\n                            if (msg.payload.substring(0,1)!=\".\") $(\"#LCDTextBody\").text(msg.payload);  \n                            else\n                            {\n                              msg.payload=msg.payload.substring(1);\n                              $(\".LCDWrapper\").css('background-color', LCDColours[msg.payload].items[1]);  $(\".LCDTextFront\").css('color', LCDColours[msg.payload].items[0]); \n                              if (LCDColours[msg.payload].items[1]==\"#000000\") $(\".LCDTextBack\").css('color',\"rgba(255,255,255,0.15)\"); else  $(\".LCDTextBack\").css('color',\"rgba(0,0,0,0.1)\");                                \n                                \n                            }\n                        }   \n            });\n    })(scope);\n\n</script>\n\n<style type=\"text/css\">\n@font-face {\n  font-family: \"D14MI\";\n  src: url(\"/myfonts/DSEG14Modern-Italic.woff\") format('woff');\n}\n\n\n.LCDWrapper{\n\tposition:relative;\n\tborder:3px solid #000;\n\tborder-radius:8px;\n\theight:66px;\n\twidth:304px;\n\tcolor: 0;\n    font-family: \"D14MI\";\n\tbackground-color:#66ac66;\n\tbox-shadow: 3px 3px 10px 0px rgba(0,0,0,0.3) inset; \n}\n\n.LCDTextBack{\n\tz-index:50; color:rgba(0,0,0,0.1); \n}\n\n.LCDTextFront{\n\tz-index:51; color:rgba(0,0,0,1);\n}\n\n.LCDTextBack,.LCDTextFront {\n   \tposition:absolute;\n\ttop:3px;\n\tleft:6px; \n\tfont-size:18px;\n}\n\n</style>\n\n<div class=\"LCDWrapper\">\n\t<span class=\"LCDTextBack\">~~~~~~~~~~~~~~~~~~~~</span>\n\t<span class=\"LCDTextFront\" id=\"LCDTextBody\" ></span>\n\n</div>\n","storeOutMessages":false,"fwdInMessages":false,"x":990,"y":1320,"wires":[[]]},{"id":"8e623ee.81f9bc","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"Goodbye","payloadType":"str","repeat":"","crontab":"","once":false,"x":800,"y":1140,"wires":[["864d97a5.30d348"]]},{"id":"33279d5b.72b122","type":"ui_group","z":"","name":"LCD Test","tab":"34cddaf3.8a9cd6","disp":true,"width":"6"},{"id":"34cddaf3.8a9cd6","type":"ui_tab","z":"","name":"testz","icon":"dashboard"}]

LCD Display for Node-Red

Pete's LCDAs must be obvious by now, I’m on a roll here. Having decided that Node-Red Dashboard is the way forward ( would in the past have used BLYNK or Imperihome) I’ve been getting to grips with the dashboard and as you’ll see if you check recent blogs I’ve had a fair bit of success with various CANVAS based libraries, getting them into the Dashboard.

Pete's LCDAnd so it was that over the weekend, MrShark contacted me with this link – a very nice, simple stand-along clock display. Well, that fired me up and so I grabbed the code and started ripping it apart.  One thing led to another and here we have a nice LCD display programmable in a wide range of (I have to say stunning) colours and able to display temperature or weather icons on the right  – and in the process I learned a few things such as what position-absolute is all about and a great deal about transparency.

Pete's LCDIn the display here you see the use of border shadows, 7-segment display fonts and transparency to give the impression of an old-fashioned LCD display – I think it does a nice job.  The code here displays the time, date and day automatically and you can inject temperature into it – but a brief look and you’ll soon realise that you could put just about anything in there – so I look forward to feedback from readers.  To make this work, from the link above I had to grab 3 fonts (DSEG7Modern-Italic.woff,  DSEG14Modern-Italic.woff and DSEG7Modern-BoldItalic.woff – also required is  DSEGWeather.woff) from the link above (into myfonts – see previous blogs for the reason I made up directory names like myfonts and mycss etc).

Pete's LCDSo – here is the code to put into a Dashboard template – you just need to inject msg.payload to set the temperature OR weather icons depending on mode – see flow – most of this is just for you to experiment with.

tmp484EThere are several spans in here – all position absolute – which means they overlap and are all relative to the top left of the main DIV – the trick with the LCD is simply the 8 figure semi-transparent – with you number or letter sitting on top of it. Works a treat I should say.  You’ll want a 6*2 size for the template… but of course you may choose to resize this completely.


tmpF88BAn important item include the z-index, I’ve used 50 and 51 arbitrarily – the original code this all came from used 100 and that ended up over-writing the Node-Red Dashboard menu! Of course if you don’t want to show temperature you could make that humidity or anything else really.

tmpA42CInjecting weather – see flow – includes: characters “0” to “9” for; background all segments, sun, cloud, rain, hard rain, snow, thunder rain, thunder hard rain, thunder, sun and cloud and finally “:” for all off.

Below is the code for the flow - make sure you have those fonts and inject this lot into a page in the dashboard - you'll need to change the tab details to suit yourself.

Oh, this page I found while looking for colour ideas…

[{"id":"5439cd84.a88ae4","type":"ui_template","z":"c552e8d2.712b48","group":"1e03a2b2.83a61d","name":"Time and Temp","order":0,"width":"6","height":"2","format":"<script>\n    var icon=\"T\";\n\n        \n    var colours={\n    \"blackOnOrange\": {items:[\"#222\",\"#fb7c00\"]},    \n    \"blackOnGreen\" : {items:[\"#222\",\"#66ac66\"]},\n    \"blackOnBlue\" : {items:[\"#222\",\"#8888ff\"]},\n    \"blackOnYellow\" : {items:[\"#222\",\"#bbbb44\"]},\n    \"blackOnWhite\" : {items:[\"#222\",\"#aaaaaa\"]},\n    \"blackOnPink\" : {items:[\"#222\",\"#ff8888\"]},\n    \"yellowOnRed\" : {items:[\"#ccaa22\",\"#aa2222\"]},\n    \"whiteOnCyan\" : {items:[\"#dddddd\",\"#227777\"]},\n    \"orangeOnBlack\" : {items:[\"#ff8800\",\"#000000\"]},  \n    \"limeOnBlack\" : {items:[\"#00cc55\",\"#000000\"]},  \n    }  \n    \n    var daylist = [\"sun\", \"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\"];\n    (function(scope){ \n            scope.$watch('msg', function(msg) {\n               if (typeof(msg.type) != \"undefined\") icon=msg.type;\n    \n               if (icon==\"t\")\n                    {\n                        $(\"#DSEGWEATHER-BACK\").text(\" \");\n                        $(\"#DSEGWEATHER-ICON\").text(\" \");\n                        $(\"#DSEGTempcF\").text(\"C\");\n                        $(\"#DSEGTempcB\").text(\"8\");                        \n                        if (typeof(msg.payload) != \"undefined\") { $(\"#DSEGTempF\").text(msg.payload);  $(\"#DSEGTempB\").text(\"88\"); }\n                    }\n                else\n                    {\n                        $(\"#DSEGTempF\").text(\"\");\n                        $(\"#DSEGTempcF\").text(\"\");\n                        $(\"#DSEGTempB\").text(\"\");\n                        $(\"#DSEGTempcB\").text(\"\");\n                        $(\"#DSEGWEATHER-BACK\").text(\"0\");\n                        if (typeof(msg.payload) != \"undefined\") $(\"#DSEGWEATHER-ICON\").text(msg.payload);\n                        \n                    }\n               if (typeof(msg.colour) != \"undefined\") {\n                              $(\".Clock-Wrapper\").css('background-color', colours[msg.colour].items[1]);  $(\".lcdClock\").css('color', colours[msg.colour].items[0]); \n                              if (colours[msg.colour].items[1]==\"#000000\") $(\".background\").css('color',\"rgba(255,255,255,0.15)\"); else  $(\".background\").css('color',\"rgba(0,0,0,0.1)\");\n                        }\n            });\n    })(scope);\n\n    function genTimerStrings(tm, num){\n    \n    \tvar i;\n    \tvar ret = tm.toString(10);\n    \tvar left = ret.length;\n    \n    \tif( left < num){\n    \t\tfor(i=0; i<( num - left ); i++ ){\n    \t\t\tret = String(0) + ret;\n    \t\t}\n    \t}\n    \treturn ret;\n    }\n\n    function updateTimer(){\n    \tvar ret;\n    \tvar date = new Date();\n    \tvar tm_year, tm_mon, tm_date, tm_hour, tm_min, tm_sec, tm_msec,tm_day;\n    \tvar colon;\n    \ttm_year = date.getFullYear();\n    \ttm_mon = date.getMonth()+1;\n    \ttm_date = date.getDate();\n    \ttm_day = date.getDay();\n    \ttm_hour = date.getHours();\n    \ttm_min = date.getMinutes();\n    \ttm_sec = date.getSeconds();\n    \ttm_msec = date.getMilliseconds();\n    \n    \ttm_mon = genTimerStrings(tm_mon, 2);\n    \ttm_date = genTimerStrings(tm_date, 2);\n    \ttm_hour = genTimerStrings(tm_hour, 2);\n    \ttm_min = genTimerStrings(tm_min, 2);\n    \ttm_sec = genTimerStrings(tm_sec, 2);\n    \ttm_day = daylist[tm_day];\n    \n    \tif( tm_msec > 499 ){\n    \t\tcolon = ' ';\n    \t}else{\n    \t\tcolon = ':';\n    \t}\n    \n    \tdocument.getElementById(\"DSEGClock\").innerHTML = tm_hour + colon + tm_min + \"<span style=\\\"font-size:30px;\\\">\"  + tm_sec + \"</span>\";\n    \tdocument.getElementById(\"DSEGClock-Year\").innerHTML = \"<span class=\\\"D7MI\\\">\" + tm_year + \"-\" + tm_mon + \"-\" + tm_date + ' ' + \"</span><span class=\\\"D14MI\\\">\" + tm_day  +  \".\" + \"</span>\";\n    \n    \tsetTimeout(\"updateTimer()\", 500 - date.getMilliseconds()%500 );\n    }\n\n    updateTimer();\n    \n</script>\n\n<style type=\"text/css\">\n.lcdClock {\n\tbackground-color:#fbfbfb;\n\tfont-size:100%;\n\tpadding-left:10px;\n\tpadding-right:10px;\n\tpadding-bottom:10px;\n\tmax-width:300px;\n\tline-height:160%;\n\tcolor:#222;\n\tfont-family:Meiryo, 'Lucida Grande','Hiragino Kaku Gothic ProN', sans-serif;\n}\n\n@font-face {\n  font-family: \"D7MI\";\n  src: url(\"/myfonts/DSEG7Modern-Italic.woff\") format('woff');\n}\n\n@font-face {\n  font-family: \"D14MI\";\n  src: url(\"/myfonts/DSEG14Modern-Italic.woff\") format('woff');\n}\n\n@font-face {\n  font-family: \"D7MBI\";\n  src: url(\"/myfonts/DSEG7Modern-BoldItalic.woff\") format('woff');\n}\n\n@font-face {\n  font-family: \"DWEATHER\";\n  src: url(\"/myfonts/DSEGWeather.woff\") format('woff');\n}\n\n.Weather-Background{\n\tz-index:50;\n\tposition:absolute;\n\ttop:24px;\n\tleft:226px;\n\tfont-size:68px;\n}\n\n.Weather-Front{\n\tz-index:51;\n\tposition:absolute;\n\ttop:24px;\n\tleft:226px;\n\tfont-size:68px;\n}\n\n.D7MI {\nfont-family: \"D7MI\";\n}\n\n.D7MBI {\nfont-family: \"D7MBI\";\n}\n\n.D14MI {\nfont-family: \"D14MI\";\n}\n\n.DWEATHER {\nfont-family: \"DWEATHER\";\nfont-size:68px;\nheight:68px;\n}\n\n.Clock-Wrapper{\n\tposition:relative;\n\tborder:6px solid #000;\n\tborder-radius:9px;\n\theight:68px;\n\twidth:280px;\n\tbackground-color:#fb7c00;\n\tbackground-color:#66ac66;\n\tbox-shadow: 4px 4px 28px 0px rgba(0,0,0,0.3) inset; \n}\n\n.Clock-Time-Background{\n\tz-index:50;\n}\n\n.Clock-Time-Front{\n\tz-index:51;\n}\n\n.Clock-Time-Background,.Clock-Time-Front {\n   \tposition:absolute;\n\ttop:38px;\n\tleft:5px; \n\tfont-size:42px;\n}\n\n.background { color:rgba(0,0,0,0.1); }\n\n.Clock-Year-Background{\n\tz-index:50;\n\tfont-size:18px;\n}\n\n.Clock-Year-Front{\n\tz-index:51;\n}\n\n.Clock-Year-Background,.Clock-Year-Front {\n   \tposition:absolute;\n\ttop:2px;\n\tleft:5px; \n\tfont-size:18px;\n}\n\n.temp { z-index:51; }\n.tempBack { z-index:50; color:rgba(0,0,0,0.1); }\n.temp,.tempBack {\n   \tposition:absolute;\n\ttop:28px;\n\tleft:210px; \n\tfont-size:42px;\n}\n\n.tempc { z-index:51; }\n.tempcBack { z-index:50;  }\n.tempc,.tempcBack {\n   \tposition:absolute;\n\ttop:36px;\n\tleft:278px; \n\tfont-size:24px;\n}\n\n#DSEG7_OUTPUT{\n\tfont-family: \"D7MI\";\n}\n\n#DSEG14_OUTPUT{\n\tfont-family: \"D14MI\";\n}\n\n#DSEG14_OUTPUT, #DSEG7_OUTPUT{\n\tfont-size:18px;\n\tmargin-top:2px;\n\tmargin-bottom:10px;\n}\n\n</style>\n\n\n<div class=\"Clock-Wrapper center lcdClock\">\n\t<span class=\"Clock-Time-Background background  D7MBI\">88:88<span style=\"font-size:30px;\">88</span></span>\n\t<span id=\"DSEGClock\" class=\"Clock-Time-Front D7MBI\"></span>\n\t<span class=\"Clock-Year-Background background\"><span class=\"D7MI\">2088-88-88</span><span class=\"D14MI\"> ~~~</span></span>\n\t<span id=\"DSEGClock-Year\" class=\"Clock-Year-Front\"></span>\n\t\n\t<span id=\"DSEGTempF\" class=\"temp D7MBI\">00</span>\n\t<span id=\"DSEGTempB\" class=\"tempBack background D7MBI\">88</span>\t\n\t<span id=\"DSEGTempcF\" class=\"tempc D7MI\">C</span>\n\t<span id=\"DSEGTempcB\" class=\"tempcBack background D7MI\">8</span>\n\t\n\t\n\t<span id=\"DSEGWEATHER-BACK\" class=\"Weather-Background background DWEATHER\"></span>\n\t<span id=\"DSEGWEATHER-ICON\" class=\"Weather-Front DWEATHER\"></span>\n\t\t\n</div>\n\n","storeOutMessages":true,"fwdInMessages":false,"x":480,"y":1480,"wires":[[]]},{"id":"e26a920b.fd916","type":"inject","z":"c552e8d2.712b48","name":"21c","topic":"","payload":"21","payloadType":"num","repeat":"","crontab":"","once":false,"x":90,"y":1220,"wires":[["5439cd84.a88ae4"]]},{"id":"a92b9b7a.795228","type":"inject","z":"c552e8d2.712b48","name":"32c","topic":"","payload":"32","payloadType":"num","repeat":"","crontab":"","once":false,"x":90,"y":1260,"wires":[["5439cd84.a88ae4"]]},{"id":"6d378317.dd743c","type":"inject","z":"c552e8d2.712b48","name":"green","topic":"","payload":"blackOnGreen","payloadType":"str","repeat":"","crontab":"","once":false,"x":90,"y":1400,"wires":[["a23865de.5f59b8"]]},{"id":"24a314bf.90927c","type":"inject","z":"c552e8d2.712b48","name":"orange","topic":"","payload":"blackOnOrange","payloadType":"str","repeat":"","crontab":"","once":false,"x":90,"y":1440,"wires":[["a23865de.5f59b8"]]},{"id":"a23865de.5f59b8","type":"function","z":"c552e8d2.712b48","name":"","func":"msg.colour=msg.payload;\nmsg.payload=undefined;\nreturn msg;","outputs":1,"noerr":0,"x":270,"y":1480,"wires":[["5439cd84.a88ae4"]]},{"id":"c8edfc9b.be6ce","type":"inject","z":"c552e8d2.712b48","name":"blue","topic":"","payload":"blackOnBlue","payloadType":"str","repeat":"","crontab":"","once":false,"x":90,"y":1480,"wires":[["a23865de.5f59b8"]]},{"id":"18a1c3a.b9c283c","type":"inject","z":"c552e8d2.712b48","name":"yellow","topic":"","payload":"blackOnYellow","payloadType":"str","repeat":"","crontab":"","once":false,"x":90,"y":1520,"wires":[["a23865de.5f59b8"]]},{"id":"a5c4dd24.4b6ff","type":"inject","z":"c552e8d2.712b48","name":"white","topic":"","payload":"blackOnWhite","payloadType":"str","repeat":"","crontab":"","once":false,"x":90,"y":1560,"wires":[["a23865de.5f59b8"]]},{"id":"83e6ddc0.ec7bd","type":"inject","z":"c552e8d2.712b48","name":"pink","topic":"","payload":"blackOnPink","payloadType":"str","repeat":"","crontab":"","once":false,"x":90,"y":1600,"wires":[["a23865de.5f59b8"]]},{"id":"ebfd33d8.c8b13","type":"inject","z":"c552e8d2.712b48","name":"red and light text","topic":"","payload":"yellowOnRed","payloadType":"str","repeat":"","crontab":"","once":false,"x":120,"y":1640,"wires":[["a23865de.5f59b8"]]},{"id":"26972f6.8af94d","type":"inject","z":"c552e8d2.712b48","name":"cyan and white","topic":"","payload":"whiteOnCyan","payloadType":"str","repeat":"","crontab":"","once":false,"x":120,"y":1680,"wires":[["a23865de.5f59b8"]]},{"id":"10449ff5.d9588","type":"inject","z":"c552e8d2.712b48","name":"Type t for TEXT","topic":"","payload":"t","payloadType":"str","repeat":"","crontab":"","once":false,"x":120,"y":1760,"wires":[["fe927255.d2e2a"]]},{"id":"3e44785a.6181b8","type":"inject","z":"c552e8d2.712b48","name":"Type w for weather","topic":"","payload":"w","payloadType":"str","repeat":"","crontab":"","once":false,"x":130,"y":1800,"wires":[["fe927255.d2e2a"]]},{"id":"fe927255.d2e2a","type":"function","z":"c552e8d2.712b48","name":"","func":"msg.type=msg.payload;\nmsg.payload=undefined;\nreturn msg;","outputs":1,"noerr":0,"x":310,"y":1740,"wires":[["5439cd84.a88ae4"]]},{"id":"94706c5b.a9292","type":"inject","z":"c552e8d2.712b48","name":"weather 2","topic":"","payload":"2","payloadType":"str","repeat":"","crontab":"","once":false,"x":100,"y":1299,"wires":[["5439cd84.a88ae4"]]},{"id":"77b95bb8.1ce774","type":"inject","z":"c552e8d2.712b48","name":"weather 3","topic":"","payload":"3","payloadType":"str","repeat":"","crontab":"","once":false,"x":100,"y":1339,"wires":[["5439cd84.a88ae4"]]},{"id":"30c75186.ef732e","type":"inject","z":"c552e8d2.712b48","name":"lime on black","topic":"","payload":"limeOnBlack","payloadType":"str","repeat":"","crontab":"","once":false,"x":110,"y":1720,"wires":[["a23865de.5f59b8"]]},{"id":"1e03a2b2.83a61d","type":"ui_group","z":"","name":"testy","tab":"f9bab960.c839b8","disp":true,"width":"6"},{"id":"f9bab960.c839b8","type":"ui_tab","z":"","name":"testy","icon":"dashboard"}]