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