因為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>

