[Node-red] Schedule node 소스 분석
2022. 11. 16. 12:27ㆍ개발/Node-red
Inject 로 시간 데이터를 입력하거나 노드 내에서 html Schedule 기능으로 동작구현.
특정 시간에 동작시키는 기능. MQTT 무선통신으로 ESP32 에 데이터전송.
Node-red 에서 원격으로 PTC 환풍기를 제어하기 위한 노드. 오류를 수정하기 위해 소스코드를 분석해보았다.
오류
기본 상태에서는 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 유/무를 반환. (하단 사진 참조)
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 |