[Node-red] Schedule node 소스 분석

2022. 11. 16. 12:27개발/Node-red

Inject 로 시간 데이터를 입력하거나 노드 내에서 html Schedule 기능으로 동작구현.
특정 시간에 동작시키는 기능. MQTT 무선통신으로 ESP32 에 데이터전송.

Node-red 에서 원격으로 PTC 환풍기를 제어하기 위한 노드. 오류를 수정하기 위해 소스코드를 분석해보았다.

 

ESP32 릴레이 4채널 보드
제어 할 PTC 환풍기
4가지 기능-LED,냉풍,온풍,환기

오류

기본 상태에서는 mqtt에 정상적으로 접속하지만, 노드 내에서 Mac address 나 schedule을 입력하면

mqtt 에 접속중... 을 반복한다.


 

MQTT 출력 데이터 포맷

function publishMessage(item, client, inTopic, config) {
        client.publish(inTopic, '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :0" + ", \"value\" :" + String(item[0]) + '}');
        client.publish(inTopic, '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :1" + ", \"value\" :" + String(item[1]) + '}');
        client.publish(inTopic, '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :2" + ", \"value\" :" + String(item[2]) + '}');
        client.publish(inTopic, '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :3" + ", \"value\" :" + String(item[3]) + '}');
        console.log(item[0], item[1], item[2], item[3]);
    }
  • publishMessage 함수. 입력받은 시간에 동작했을 때 mqtt inTopic에 객체 데이터를 전송한다.
  • "outNo" key는 ESP32 보드의 릴레이 출력 4가지 선언, "value" key는 노드 설정에서 checkbox 유/무를 반환. (하단 사진 참조)

4가지 기능 동작


MQTT 및 timestamp변수 선언

RED.nodes.createNode(this,config);
        var node = this;
        var received = "";
        var mqtt    = require('mqtt');
        var client  = mqtt.connect("mqtt://broker.mqtt-dashboard.com");
        var curTimestamp = 0;
        var sTimestamp = 0;
        var eTimestamp = 0;
        var nodeContext = this.context();
        var outTopic= "LightTalk-" + String(config.id) + "-out";
        var inTopic = "LightTalk-" + String(config.id) + "-in";
        var curTimestamp = Date.now();
 
  • mqtt Topic 및 schedule 기능을 위한 시간 변수들을 생성.
  • 노드 소스 디렉토리에 npm install mqtt 로 설치된 mqtt 기능. 
  • Schedule 기능을 위한 Timestamp 관련 변수 선언.
  • mqtt 통신을 위한 outTopic, inTopic 을 config.id 로 구분해 고유한 topic을 사용.
  • mqtt 서버는 broker.mqtt-dashboard.com 사용 ( 공용 오픈 서버 )

Schedule 입력 변수 및 checkbox 배열 선언

var start = String(config.startTime);
var end = String(config.endTime);
var item = [];
  • config.startTime, config.endTime 은 노드를 클릭했을 때 나타나는 시간입력 값. ( 하단 사진참조 )
  • item 배열은 최상단에 기술했던 노드 설정 내 checkbox 선택 유/무.


4채널 릴레이 초기화 데이터 포맷

var offLoad = [
            '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :0" + ", \"value\" :0" + '}',
            '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :1" + ", \"value\" :0" + '}',
            '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :2" + ", \"value\" :0" + '}',
            '{\"mac\":' + config.id + ", \"type\":100" + ", \"outNo\" :3" + ", \"value\" :0" + '}'
        ]; //turns all ports off
  • 모든 릴레이 출력을 off 하도록 해주는 데이터 offLoad 배열.

입력값 유/무 구분

sTimestamp = new Date(start).getTime(); //stime timestamp
eTimestamp = new Date(end).getTime(); //etime timestamp

if (!sTimestamp && !eTimestamp) { //form is empty, bring inputs to the current schedule
     console.log('warning');
     node.warn('전달된 페이로드가 없습니다. 스케쥴 날짜를 페이로드로 넘기거나, 노드 설정에서 스케쥴 날짜를 선택해주세요.');
     //node does not have the property of the schedule inputs, so warn the user.
     }

     else {
     item.push(config.heat);
     item.push(config.cool);
     item.push(config.exha);
     item.push(config.led);
     }

node.warn('등록하신 스케쥴을 시작합니다. (' + String(node.start_time) + '~' + String(node.end_time) + ')');
  •  노드 설정에서 입력했던 schedule 시간의 timestamp 값을 반환하는 변수 sTimestamp, eTimestamp
  •  저장된 시간 값이 없을 경우 if 문 ( sTimestamp,eTimestamp 둘다 없을 때 )
  •  else 는 item 배열에 체크박스( heating, cooling, exhaust, led ) 유/무 push
  •  처리 후 Node-red의 msg.payload 란에 메시지 출력.
  •  추후에 타이머가 동작할 때, 이 부분을 거치면서 heat,cool,exha,led 기능을 반환함.

Schedule 에 따른 동작 처리 - 1 ( 타이머 시작 전 )

if (curTimestamp < sTimestamp) { //timer hasn't started (considering the case where the flow has been restarted)
       setTimeout(function() {
          publishMessage(item, client, inTopic, config);
       }, sTimestamp - curTimestamp);
                
       setTimeout(function() {
           for (var i = 0; i < 4; i++) {
               client.publish(inTopic, offLoad[i]);
           }
       }, eTimestamp - curTimestamp); //turn all ports off when the schedule has been finished.

node.warn("등록하신 스케쥴 " + String(node.start_time) + '~' + String(node.end_time) + '이 끝났습니다.')
}
  • curTimestamp ( Date.now() ) 가 sTimestamp ( 입력받은 시간 시간 ) 보다 작을 때 -> 타이머 시작 전.
  • 첫번째 setTimeout의 첫 인자에 데이터 출력 함수, 두번째 인자에 시작시간 - 현재시간, 즉 시작시간까지 delay.
  • 두번째 settimeout의 첫 인자에 초기화 데이터 출력 함수, 두번째 인자에 종료시간 - 현재시간, 즉 종료시간에 초기화 데이터 MQTT 송신.

Schedule 에 따른 동작 처리 - 2 ( 타이머 진행 중 )

else if (curTimestamp >= sTimestamp && curTimestamp <= eTimestamp) { //schedule has been up and running
       //{"mac":"048e741c5210","type":100,"outNo":2,"value":1}
       publishMessage(item, client, inTopic, config);
       setTimeout(function() {
           for (var i = 0; i < 4; i++) {
               client.publish(inTopic, offLoad[i]);
           }
       }, eTimestamp - curTimestamp); //reset all ports
            
       node.warn("등록하신 스케쥴 " + String(node.start_time) + '~' + String(node.end_time) + '이 끝났습니다.');    
}
  • 현재 시간이 input된 시작 시간보다 크고, input된 종료 시간보다 작을 때, 즉 타이머 기능 실행 도중
  • 입력값 유/무 구분 카테고리에서 사용된 함수에서 heat,cool,exhau,led 값을 저장하고 publishMessage 실행
  • setTimeout의 첫 인자에 초기화 데이터 MQTT 송신, 두번째 인자에 종료시간 - 현재시간, 즉 종료시간에 초기화 데이터 MQTT 송신

Schedule 에 따른 동작 처리 - 3 ( 타이머 종료 )

else if (curTimestamp > eTimestamp) {
       for (var i = 0; i < 4; i++) {
           client.publish(inTopic, offLoad[i]);
       } //reset all ports since the schedule has already been finished. Just in case the ports were not reset.
}
  • 현재 시간이 종료 시간보다 클 때, 즉 타이머 종료 시, 초기화 데이터 MQTT 송신

Node-red 상태에 따른 처리 - 1 ( 초기 실행 )

node.on('close', function() {
            node.connstatusSet = 0;
            curTimestamp = Date.now();

            /*
            //a very dirty solution, but we don't have the info of the IDs so just manually clear all random timeouts.
            for (let i = 0; i< 100000; i++) {
                clearTimeout(i);
            } //do not add this line if the user wants to keep the schedule set before.
            */

            node.status({}); //clear the status of a node
            node.status({fill:"yellow",shape:"ring",text:"연결 중.."});
            client.end();
            client  = mqtt.connect("mqtt://broker.mqtt-dashboard.com");
            // tidy up any state
        });
  • 상태 처리 connstatusSet = 0.
  • curTimestamp 에 현재 시간 저장.
  • node.status 명령어는 노드 하단에 나타나는 문구 처리. mqtt broker 연결 전 연결 중...문구 출력
  • mqtt 서버에 접속

Node-red 상태에 따른 처리 - 2 ( mqtt 접속 )

client.on("connect",function(){	
            console.log("connected  "+ client.connected);
            nodeContext.set("isValidMsg", false); //reset the status
            console.log(outTopic);
            client.subscribe(outTopic,{qos:0}); 
            if(client.connected==true)
                node.status({fill:"green",shape:"dot",text:"접속됨"});
            else
                node.status({fill:"red",shape:"ring",text:"절단"});
        });
  • mqtt 에 연결 후 outTopic 을 subscribe.
  • 노드 하단에 초록색 박스 및 '접속됨' 메시지 출력.

Node-red 상태에 따른 처리 - 3 ( 에러 )

client.on("error",function(error){
	        console.log("Can't connect" + error);
	        process.exit(1)}
);
  • 에러 발생했을 시 process.exit(1) 명령어로 플로우 중지.

Node-red 상태에 따른 처리 - 4 ( inject input )

node.on('input', function(msg) {
       console.log(msg.payload);
       var pLoad = JSON.parse(msg.payload);

       if (sTimestamp || eTimestamp) {
           node.warn('페이로드로 날짜를 전달할 때는 노드 설정의 스케쥴을 비워주세요.');
       }

       else if (pLoad.start_time != undefined && pLoad.end_time != undefined) {
           //{"start_time" : "", "end_time" : "" ,"outNo" : 2, "value" : 1} -> {"mac":"048e741c5210","type":100,"outNo":2,"value":1}
           node.start_time = pLoad.start_time;
           node.end_time = pLoad.end_time;

           var stime = new Date(node.start_time).getTime();
           var etime = new Date(node.end_time).getTime();


           var cur = Date.now();

           node.warn('등록하신 스케쥴을 시작합니다. (' + String(node.start_time) + '~' + String(node.end_time) + ')');
           var msg_toPublish = '{\"mac\" : ' + "\"" + String(config.id) + "\",\"type\" : 100,\"outNo\" : " + String(pLoad.outNo) + ",\"value\" : " + String(pLoad.value) + "}"; 
           if (cur < stime) { //timer hasn't started (considering the case where the flow has been restarted)
               setTimeout(function() {
                   client.publish(inTopic, msg_toPublish);
               }, stime - cur);
                        
               setTimeout(function() {
                   for (var i = 0; i < 4; i++) {
                        client.publish(inTopic, offLoad[i]);
                   }
               }, etime - cur); //turn all ports off when the schedule has been finished.
               node.warn("등록하신 스케쥴 " + String(node.start_time) + '~' + String(node.end_time) + '이 끝났습니다.');
           } else if (cur >= stime && cur <= etime) { //schedule has been up and running
               //{"mac":"048e741c5210","type":100,"outNo":2,"value":1}
               client.publish(inTopic, msg_toPublish);
               setTimeout(function() {
                   for (var i = 0; i < 4; i++) {
                       client.publish(inTopic, offLoad[i]);
                   }
               }, etime - cur); //reset all ports
               node.warn("등록하신 스케쥴 " + String(node.start_time) + '~' + String(node.end_time) + '이 끝났습니다.');
           } else if (cur > etime) {
               for (var i = 0; i < 4; i++) {
                   client.publish(inTopic, offLoad[i]);
               } //reset all ports since the schedule has already been finished. Just in case the ports were not reset.
           } else {
               node.send("nothing happened");
           } //do nothing since case does not exist.
       }
       //client.publish(inTopic, msg.payload);
       //node.send(msg);
});
  • Node 에 inject 값이 들어왔을 때 pLoad 에 들어온 msg 를 Object 로 반환한다.
if (sTimestamp || eTimestamp) {
           node.warn('페이로드로 날짜를 전달할 때는 노드 설정의 스케쥴을 비워주세요.');
}
  • 외부에서 시간 값을 input할 때는 노드 내의 schedule 설정을 사용 안 하므로 비워두도록 메시지 출력 
else if (pLoad.start_time != undefined && pLoad.end_time != undefined) {
  • input 메시지의 시작시간이 존재하고, 종료시간도 존재하면
node.start_time = pLoad.start_time;
node.end_time = pLoad.end_time;

var stime = new Date(node.start_time).getTime();
var etime = new Date(node.end_time).getTime();

var cur = Date.now();

node.warn('등록하신 스케쥴을 시작합니다. (' + String(node.start_time) + '~' + String(node.end_time) + ')');
var msg_toPublish = '{\"mac\" : ' + "\"" + String(config.id) + "\",\"type\" : 100,\"outNo\" : " + String(pLoad.outNo) + ",\"value\" : " + String(pLoad.value) + "}";
  • node.start_time, end_time 변수에 input된 시작, 종료시간을 저장
  • stime, etime에는 저장된 시간의 timestamp 값을 저장
  • cur 에는 현재 시간 저장
  • node-red의 메시지에 시작~종료 시간 출력
  • msg_toPublish 에 출력 데이터 포맷

타이머 시작 전

 

if (cur < stime) { //timer hasn't started (considering the case where the flow has been restarted)
     setTimeout(function() {
         client.publish(inTopic, msg_toPublish);
     }, stime - cur);
                        
     setTimeout(function() {
         for (var i = 0; i < 4; i++) {
             client.publish(inTopic, offLoad[i]);
         }
     }, etime - cur); //turn all ports off when the schedule has been finished.
     node.warn("등록하신 스케쥴 " + String(node.start_time) + '~' + String(node.end_time) + '이 끝났습니다.');
}
  • 현재 시간이 시작 시간보다 작을 때, 즉 타이머 시작 전 . 노드 설정 에서 schedule 로 데이터 처리할때와 동일함
  • 시작시간 - 현재시간 delay 후 MQTT 로 출력데이터 ( msg_toPublish ) 송신
  • 종료시간 - 현재시간 delay 후 MQTT 로 초기화데이터 ( offLoad[ i ] ) 송신
  • Node-red 메시지 출력에 종료 메시지 출력

타이머 진행 중

else if (cur >= stime && cur <= etime) { //schedule has been up and running
       //{"mac":"048e741c5210","type":100,"outNo":2,"value":1}
         client.publish(inTopic, msg_toPublish);
         setTimeout(function() {
         for (var i = 0; i < 4; i++) {
              client.publish(inTopic, offLoad[i]);
          }
         }, etime - cur); //reset all ports
node.warn("등록하신 스케쥴 " + String(node.start_time) + '~' + String(node.end_time) + '이 끝났습니다.');
}
  • 현재 시간이 시작 시간보다 크고, 종료 시간보다 작을 때. 즉 타이머 진행 중
  • MQTT로 출력 데이터 ( msg_toPublish ) 송신
  • 종료시간-현재시간 delay 후 MQTT로 초기화데이터 ( offLoad[ i ] ) 송신
  • Node-red 메시지 출력에 종료 메시지 출력

타이머 종료

else if (cur > etime) {
    for (var i = 0; i < 4; i++) {
        client.publish(inTopic, offLoad[i]);
    } //reset all ports since the schedule has already been finished. Just in case the ports were not reset.
}
  • 현재 시간이 종료 시간보다 클 때, 즉 타이머 종료
  • MQTT로 초기화데이터 ( offLoad[ i ] ) 송신 

MQTT 수신 처리

node.connstatusSet = 0;
var outputs = [0, 0, 0, 0];

client.on('message',function(outTopic, msg, packet){
            console.log("connStatus : " + node.connstatusSet);
            if (!node.connstatusSet) {
                node.status({fill:"green",shape:"dot",text:"연결됨"});
                node.connstatusSet = 1;
            }
            client.publish("LightTalk-sendMac", inTopic); //send Node-RED the topic with the MAC Addr.
            nodeContext.set("isValidMsg", true);
            console.log(msg != null);
            console.log("Message arrived : " + msg);
            console.log("With the topic of "+ outTopic);
            received = JSON.parse(String(msg));
            for (var i = 0; i < 4; i++)
                outputs[i] = Number(received.out[i]); 
            var msg1 = {};
            var msg2 = {};
            var msg3 = {};
            var msg4 = {};
            msg1.payload = outputs[0];
            msg2.payload = outputs[1];
            msg3.payload = outputs[2];
            msg4.payload = outputs[3];
            node.send([msg1, msg2, msg3, msg4]);
        });
  • ESP32에 사용될 아두이노 소스에서 MQTT 송신하는 데이터 수신처리
  • node.connstatusSet은 상태를 나타내기 위한 값
  • outputs 배열은 msg.payload 출력을 위한 배열
  • received 에 수신받은 msg 값을 Object로 반환.
  • msg1, msg2, msg3, msg4 에 sort 및 payload 로 출력.

'개발 > Node-red' 카테고리의 다른 글

[Node-red] Schedule node 수정 - 4  (0) 2022.11.21
[Node-red] Schedule node 수정 - 3  (0) 2022.11.21
[Node-red] Schedule node 수정 - 2  (0) 2022.11.17
[Node-red] Schedule node 수정-1  (0) 2022.11.16
Node-red 윈도우 설치  (0) 2022.11.14