prettyprint

2021年11月2日 星期二

基於Node-RED Dashboard ui-template設計的指針時鐘(A Node-RED Dashboard analog clock base on ui-template)

 因為Node-RED Dashboard的ui-template可以帶html與Angular/Angular-Material指令(The template widget can contain any valid html and Angular/Angular-Material directives),所以很容易利用ui-template擴充UI介面。本實驗來時做一個指針時鐘Node。

Node-RED Dashboard ui-template接受msg input 與 output msg如下圖所示,所以很容易與其他node連接flow。





本實驗使用canvas API作為繪圖工具,完成需具有下列功能:
  1. 能隨意設定時鐘顯示的大小。
  2. Subscribe MQTT topic來設定指針時鐘的鬧鐘時間與是否顯示日期,鬧鐘啟動時閃爍時鐘畫面並送出TTS字串給Dashboard Audio out播放。
  3. 可以獨立的UI node運作,也可與其他node連結。
藉由Subscribe MQTT Topic來達成設定鬧鐘與顯示日期,當鬧鐘時間到達時送出TTS字串給Audio out Node播放聲音,指針時鐘畫面閃爍30秒。
MQTT topic: MY_CLOCK。
MQTT payload: 
  1. {"type":"alarm", "data":{"h":1,"m":1}}   //設定鬧鐘時間
  2. {"type":"show", "data":1}                       //設定顯示日期
  3. {"type":"show", "data":0}                       //設定關閉顯示日期

實驗結果展示如下:


Dashboard ui-template程式碼:
<script>
var ctx;
var canvas;
var clockAlarm=[];
var alarmStartCount=0;
var clockLoaded=false;
var clockScale=0;
var apm="AM";
var weekday=["星期日","星期一","星期二","星期三","星期四","星期五","星期六"];
var weekdayString="";
var dateString="";
var showDate=false;
(function(scope) {
    scope.$watch('msg', function(msg) {
        if (!clockLoaded) {clockLoaded=true; scope.loadClock();}
        if (msg ) {
            // which msg topic wanted to process
            if (msg.topic == "MY_CLOCK") {
                if (msg.payload.type=="alarm") {
                    clockAlarm.push(msg.payload.data);    
                }
                if (msg.payload.type=="show") {
                    showDate=msg.payload.data;
                }
            }
            // which msg topic wanted to process
        } 
    });
    
   scope.baseClock = function () {
        ctx.save();
        ctx.clearRect(0, 0, canvas.width, canvas.width);
        ctx.translate(canvas.width/2,canvas.width/2);
        ctx.scale(clockScale, clockScale);
     
        ctx.strokeStyle = 'black';
        ctx.fillStyle = 'white';
        ctx.lineWidth = 8;
        ctx.lineCap = 'round';
          
        // draw clock border
        ctx.save()
        ctx.beginPath();
        ctx.lineWidth = 14;
        //ctx.strokeStyle = '#123F82';
        ctx.strokeStyle = '#409696';
        ctx.fillStyle="#FFFFFF";
        if (alarmStartCount > 0) {
            if (alarmStartCount % 2==0) {
                ctx.fillStyle="#FFBCBC";
            }
            alarmStartCount--;
        }
        ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
        ctx.fill();
        ctx.stroke();
        if (showDate) {
            ctx.fillStyle="#000000";
            ctx.font = '24px serif';
            ctx.fillText(apm,-16, 80)
            ctx.fillStyle="#B45A00";
            ctx.font = '18px serif';
            ctx.fillText(dateString, 0-dateString.length/2*9, -65)
            ctx.fillText(weekdayString, 0-weekdayString.length/2*18,-45);
        }
        ctx.restore();
        
      // Hour marks
      ctx.save();
      for (var i = 0; i < 12; i++) {
          if (i%3==2) {
              ctx.strokeStyle = 'red';
              ctx.lineWidth = 10;
           }else {
              ctx.strokeStyle = 'black';
              ctx.lineWidth = 8;
           }
        ctx.beginPath();
        ctx.rotate(Math.PI / 6);
        ctx.moveTo(100, 0);
        ctx.lineTo(120, 0);
        ctx.stroke();
      }
      ctx.restore();
      
      // Minute marks
      ctx.save();
      ctx.lineWidth = 5;
      for (i = 0; i < 60; i++) {
        if (i % 5!= 0) {
          ctx.beginPath();
          ctx.moveTo(117, 0);
          ctx.lineTo(120, 0);
          ctx.stroke();
        }
        ctx.rotate(Math.PI / 30);
      }
      ctx.restore();
      
      ctx.restore();
       
    }
    scope.clock = function() {
      var now = new Date();
      var hr24=0;
      
      var sec = now.getSeconds();
      var min = now.getMinutes();
      var hr24  = now.getHours();
      
      var hr = hr24 >= 12 ? hr24 - 12 : hr24;
      apm = hr24 >= 12 ? "PM" : "AM";
      var m = now.getMonth()+1;
      var d = now.getDate();
      var ms = m < 10? "0"+m:m;
      var ds = d < 10? "0"+d:d;
      dateString = now.getFullYear()+"/"+ms+"/"+ds;
      weekdayString = weekday[now.getDay()];
      scope.baseClock();
      
      ctx.save();
      ctx.translate(canvas.width/2,canvas.width/2);
      
      ctx.scale(clockScale, clockScale);
      ctx.rotate(-Math.PI / 2);
      ctx.strokeStyle = 'black';
      ctx.fillStyle = 'white';
      ctx.lineWidth = 8;
      ctx.lineCap = 'round';
    
      // write Hours
      ctx.save();
      ctx.fillStyle = 'black';
      ctx.rotate(hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) *sec);
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(-15,-10);
      ctx.lineTo(80,-2);
      ctx.lineTo(80,2);
      ctx.lineTo(-15,10);
      ctx.arc(-15,0,10,Math.PI/2*3,Math.PI/2,true);
      ctx.fill();
      ctx.restore();
    
      // write Minutes
      ctx.save();
      ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
      ctx.lineWidth = 1;
      ctx.fillStyle = 'blue';
      ctx.beginPath();
      ctx.moveTo(-25,-7);
      ctx.lineTo(110,-2);
      ctx.lineTo(110,2);
      ctx.lineTo(-25,7);
      ctx.arc(-25,0,7,Math.PI/2*3,Math.PI/2,true);
      ctx.fill();
      ctx.restore();
    
      // Write seconds
      ctx.save();
      ctx.rotate(sec * Math.PI / 30);
      ctx.strokeStyle = '#D40000';
      ctx.fillStyle = '#D40000';
      ctx.lineWidth = 6;
      ctx.beginPath();
      ctx.moveTo(-30, 0);
      ctx.lineTo(83, 0);
      ctx.stroke();
      ctx.beginPath();
      ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
      ctx.stroke();
      ctx.fillStyle = 'rgba(0, 0, 0, 0)';
      ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
      ctx.fill();
      ctx.restore();
      ctx.restore();
     
      ////// send out msg begin here
      // case 1 Clock Alarm msg.topic=CLOCK_ALARM
      if (sec==0) {
          for (var i=0; i < clockAlarm.length; i++) {
              
              if (clockAlarm[i].h == hr24 && clockAlarm[i].m == min) {
                  alarmStartCount=30;
                  clockAlarm.splice(i,1);
                  var announce_string = "鬧鐘時間"+hr24+"點"+min+"分";
                  scope.send({payload:announce_string,topic:"CLOCK_ALARM_AUDIO"});
                  scope.send({payload:announce_string,topic:"CLOCK_ALARM_AUDIO"});
                  scope.send({payload:announce_string,topic:"CLOCK_ALARM_AUDIO"});
                  break;
              }
              
          }
          //scope.send({payload:{h:hr24,m:min,s:sec},topic:"audio"});
          
      }
      //////////// send out msg end 
    }
    
    scope.loadClock = function() {
        setTimeout(function() {
            canvas = document.getElementById('clockCanvas');
            var templateWidth=$("#clockCanvas").parent().width();
            var templateHeight=$("#clockCanvas").parent().height();
            var minWidth=Math.min(templateWidth, templateHeight);
            clockScale = minWidth/300;
            canvas.width=minWidth;
            canvas.height=minWidth;
            ctx = canvas.getContext('2d');
            ctx.clearRect(0,0,canvas.width,canvas.height)
            setInterval(scope.clock,1000);
        },500);
    }

})(scope);

</script>
<div><canvas id="clockCanvas" wdth="300" height="300"></canvas></div>