카테고리 없음

Node-RED 노드 만들기 3. 플로우 실행을 위한 Injection 노드 만들기

ㅈ현 2022. 10. 12. 15:05

 

 

Inject 노드를 사용한 플로우 시작

 

Inject란 플로우를 시작하기 위한 신호이며, http post 요청으로 이루어진다.
Node-RED의 기본 노드인 Inject 노드를 사용하여 Inject를 트리거 할 수 있다.

 

플로우를 시작하기 위해선 주입(Inject)이 이루어져야 한다. 이러한 Inject는 기본 노드인 Inject노드로 할 수 있고, 만들수도 있다.

다만 이러한 Inject가 없다면, 플로우가 있어도 해당 플로우를 시작 할 수 없다.

 

이번 포스팅에서는 이러한 플로우의 첫번째 노드, 즉 시작이 되는 노드를 어떻게 만드는지에 대해 알아본다.

 

Inject는 사용자가 원하는 시점에 수동으로 진행될수도, 타이머를 걸어두어 자동으로 진행될수도 있다.

 

 

반복을 통한 플로우 시작
수동 (클릭)을 통한 플로우 시작

 

수동 (클릭) Injection

클릭을 통해 플로우를 시작하는 노드를 만들어 본다.

 

노드의 Inject는 http post request 를 통해 이루어진다.

경로를 /inject/{nodeid}로 post 요청을 보내게 되면, 해당 id를 가진 노드로부터 injection이 행해진다.

Inject Post Request

네트워크를 확인해 보면 inject 요청인 간것을 확인할 수 있다.

 

html 코드

더보기
<script type="text/html" data-template-name="test-node">
</script>
<style>

</style>

<script type="text/javascript">
    RED.nodes.registerType('test-node',{
        color:"pink",
        category: "test",
        inputs:0,
        outputs:1,
        button: {
            onclick: function(){
                $.ajax({
                    url: "inject/" + this.id,
                    type: "POST",
                    contentType: "application/json; charset=utf-8",
                    success: function (resp) {
                        console.log('inject success : ', resp)
                    },
                    error: function (jqXHR, textStatus, errorThrown) {
                        console.log('inject error : ', textStatus, errorThrown)
                    }
                });
            }
        }
    });
</script>

inject 설명을 위해 노드의 다른 요소는 전부 지우고, 오로지 inject를 위한 코드만 남겨두었다.

 

button 필드의 경우 노드의 우측에 작은 버튼을 하나 만들어 준다.

button

button 필드의 onclick필드는 버튼을 클릭했을때의 동작을 정의한다.

해당 노드 클릭시 "/inject/{nodeid}" 로 post요청이 가게 되고, 서버에선 해당 요청을 받아들여, 플로우의 해당 노드를 트리거 하게 된다.

 

 

js 코드

더보기
module.exports = function(RED) {
    "use strict";
    function TestNode(config) {
        RED.nodes.createNode(this,config);

        this.on('input', (msg, send, done) => {
            msg.config = config
            this.send(msg)
            if (done){
                done();
            }
        })
    }

    RED.nodes.registerType("test-node",TestNode);

    RED.httpAdmin.post("/inject/:id", RED.auth.needsPermission("inject.write"), function(req,res) {
        var node = RED.nodes.getNode(req.params.id);
        if (node != null) {
            try {
                if (req.body && req.body.__user_inject_props__) {
                    node.receive(req.body);
                } else {
                    node.receive();
                }
                res.sendStatus(200);
            } catch(err) {
                res.sendStatus(500);
                node.error(RED._("inject.failed",{error:err.toString()}));
            }
        } else {
            res.sendStatus(404);
        }
    });
}

짝이되는 js 파일이다.

 

input 이벤트 리스너는 서버로 보낸 post요청을 받기위해 필요하다. 서버로 보낸 post요청은 해당 노드를 트리거 하게 되고, input 이벤트 리스너가 이 핸들러 역할을 하게 된다.

 

이렇게 만들어진 노드를 package.json에 등록 후 실제 모습을 보게 되면 다음과 같다.

test-node가 이번에 만든 Inject노드가 되고, 이 노드를 트리거 했을때 어떠한 결과가 나오는지를 확인하기 위해 debug노드를 연결하였다.

 

이제 이 test-node를 클릭하게 되면 input 이벤트 리스너에 정의된 내용 (config를 msg의 필드에 전달)이 실행되게 된다.

 

 

자동 (반복) Injection

위에선 수동으로 플로우를 시작하는 방법에 대해 알아보았고, 여기선 자동으로 노드를 시작하기 위한 방법을 설명한다.

예시는 Node-Red의 기본 노드인 Inject노드로부터 가져왔다.

 

Inject는 역시 http post요청을 통해 이루어진다.

반복 Inject는 일정한 주기를 등록하여, 해당 주기가 될때마다 post요청을 통해 Inject를 실행하는 방법으로 이루어진다.

 

html 코드

더보기
<script type="text/html" data-template-name="test-node">
    <!--반복을 위한 태그 start-->
    <div class="form-row">
        <label for=""><i class="fa fa-repeat"></i> <span data-i18n="td-input.label.repeat">반복</span></label>
        <select id="inject-time-type-select">
            <option value="none" data-i18n="td-input.none">없음</option>
            <option value="interval" data-i18n="td-input.interval">시간간격</option>
            <option value="interval-time" data-i18n="td-input.interval-time">시간, 일시</option>
            <option value="time" data-i18n="td-input.time">일시</option>
        </select>
        <input type="hidden" id="node-input-repeat">
        <input type="hidden" id="node-input-crontab">
    </div>

    <div class="form-row inject-time-row hidden" id="inject-time-row-interval">
        <span data-i18n="td-input.every"></span>
        <input id="inject-time-interval-count" class="inject-time-count" value="1"></input>
        <select style="width:100px" id="inject-time-interval-units">
            <option value="s" data-i18n="td-input.seconds">초</option>
            <option value="m" data-i18n="td-input.minutes">분</option>
            <option value="h" data-i18n="td-input.hours">시간</option>
        </select><br/>
    </div>

    <div class="form-row inject-time-row hidden" id="inject-time-row-interval-time">
        <span data-i18n="td-input.every">시간간격</span>
        <select style="width:90px; margin-left:20px;" id="inject-time-interval-time-units" class="inject-time-int-count" value="1">
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
            <option value="5">5</option>
            <option value="6">6</option>
            <option value="10">10</option>
            <option value="12">12</option>
            <option value="15">15</option>
            <option value="20">20</option>
            <option value="30">30</option>
            <option value="0">60</option>
        </select>
        <span data-i18n="td-input.minutes">분</span><br/>
        <span data-i18n="td-input.between">시각</span>
        <select id="inject-time-interval-time-start" class="inject-time-times"></select>
        <span data-i18n="td-input.and">~</span>
        <select id="inject-time-interval-time-end" class="inject-time-times"></select>
        <br/>
        <div id="inject-time-interval-time-days" class="inject-time-days" style="margin-top:5px">
            <div style="display:inline-block; vertical-align:top; margin-right:5px;" data-i18n="td-input.on">on</div>
            <div style="display:inline-block;">
                <div>
                    <label><input type='checkbox' checked value='1'/> <span data-i18n="td-input.days.0">월요일</span></label>
                    <label><input type='checkbox' checked value='2'/> <span data-i18n="td-input.days.1">화요일</span></label>
                    <label><input type='checkbox' checked value='3'/> <span data-i18n="td-input.days.2">수요일</span></label>
                </div>
                <div>
                    <label><input type='checkbox' checked value='4'/> <span data-i18n="td-input.days.3">목요일</span></label>
                    <label><input type='checkbox' checked value='5'/> <span data-i18n="td-input.days.4">금요일</span></label>
                    <label><input type='checkbox' checked value='6'/> <span data-i18n="td-input.days.5">토요일</span></label>
                </div>
                <div>
                    <label><input type='checkbox' checked value='0'/> <span data-i18n="td-input.days.6">일요일</span></label>
                </div>
            </div>
        </div>
    </div>

    <div class="form-row inject-time-row hidden" id="inject-time-row-time">
        <span data-i18n="td-input.at">시각</span> <input type="text" id="inject-time-time" value="12:00"></input><br/>
        <div id="inject-time-time-days" class="inject-time-days">
            <div style="display:inline-block; vertical-align:top; margin-right:5px;" data-i18n="td-input.on">on</div>
            <div style="display:inline-block;">
                <div>
                    <label><input type='checkbox' checked value='1'/> <span data-i18n="td-input.days.0">월요일</span></label>
                    <label><input type='checkbox' checked value='2'/> <span data-i18n="td-input.days.1">화요일</span></label>
                    <label><input type='checkbox' checked value='3'/> <span data-i18n="td-input.days.2">수요일</span></label>
                </div>
                <div>
                    <label><input type='checkbox' checked value='4'/> <span data-i18n="td-input.days.3">목요일</span></label>
                    <label><input type='checkbox' checked value='5'/> <span data-i18n="td-input.days.4">금요일</span></label>
                    <label><input type='checkbox' checked value='6'/> <span data-i18n="td-input.days.5">토요일</span></label>
                </div>
                <div>
                    <label><input type='checkbox' checked value='0'/> <span data-i18n="td-input.days.6">일요일</span></label>
                </div>
            </div>
        </div>
    </div>
    <!--반복을 위한 태그 end-->
</script>
<style>

</style>

<script type="text/javascript">
    RED.nodes.registerType('test-node',{
        color:"pink",
        category: "test",
        defaults: {
            //반복을 위한 변수
            repeat: {value:"", validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 2147483))) }},
            crontab: {value:""},
            //반복을 위한 변수
        },
        inputs:0,
        outputs:1,
        oneditprepare: function() {
            // oneditprepare 반복을 위한 스크립트 start
            var node = this;

            $("#inject-time-type-select").on("change", function() {
                $("#node-input-crontab").val('');
                var id = $("#inject-time-type-select").val();
                $(".inject-time-row").hide();
                $("#inject-time-row-"+id).show();

                // Scroll down
                var scrollDiv = $("#dialog-form").parent();
                scrollDiv.scrollTop(scrollDiv.prop('scrollHeight'));
            });


            $(".inject-time-times").each(function() {
                for (var i=0; i<24; i++) {
                    var l = (i<10?"0":"")+i+":00";
                    $(this).append($("<option></option>").val(i).text(l));
                }
            });
            $("<option></option>").val(24).text("00:00").appendTo("#inject-time-interval-time-end");
            $("#inject-time-interval-time-start").on("change", function() {
                var start = Number($("#inject-time-interval-time-start").val());
                var end = Number($("#inject-time-interval-time-end").val());
                $("#inject-time-interval-time-end option").remove();
                for (var i=start+1; i<25; i++) {
                    var l = (i<10?"0":"")+i+":00";
                    if (i==24) {
                        l = "00:00";
                    }
                    var opt = $("<option></option>").val(i).text(l).appendTo("#inject-time-interval-time-end");
                    if (i === end) {
                        opt.attr("selected","selected");
                    }
                }
            });

            $(".inject-time-count").spinner({
                //max:60,
                min:1
            });

            var repeattype = "none";
            if (node.repeat != "" && node.repeat != 0) {
                repeattype = "interval";
                var r = "s";
                var c = node.repeat;
                if (node.repeat % 60 === 0) { r = "m"; c = c/60; }
                if (node.repeat % 1440 === 0) { r = "h"; c = c/60; }
                $("#inject-time-interval-count").val(c);
                $("#inject-time-interval-units").val(r);
                $("#inject-time-interval-days").prop("disabled","disabled");
            } else if (node.crontab) {
                var cronparts = node.crontab.split(" ");
                var days = cronparts[4];
                if (!isNaN(cronparts[0]) && !isNaN(cronparts[1])) {
                    repeattype = "time";
                    // Fixed time
                    var time = cronparts[1]+":"+cronparts[0];
                    $("#inject-time-time").val(time);
                    $("#inject-time-type-select").val("s");
                    if (days == "*") {
                        $("#inject-time-time-days input[type=checkbox]").prop("checked",true);
                    } else {
                        $("#inject-time-time-days input[type=checkbox]").removeAttr("checked");
                        days.split(",").forEach(function(v) {
                            $("#inject-time-time-days [value=" + v + "]").prop("checked", true);
                        });
                    }
                } else {
                    repeattype = "interval-time";
                    // interval - time period
                    var minutes = cronparts[0].slice(2);
                    if (minutes === "") { minutes = "0"; }
                    $("#inject-time-interval-time-units").val(minutes);
                    if (days == "*") {
                        $("#inject-time-interval-time-days input[type=checkbox]").prop("checked",true);
                    } else {
                        $("#inject-time-interval-time-days input[type=checkbox]").removeAttr("checked");
                        days.split(",").forEach(function(v) {
                            $("#inject-time-interval-time-days [value=" + v + "]").prop("checked", true);
                        });
                    }
                    var time = cronparts[1];
                    var timeparts = time.split(",");
                    var start;
                    var end;
                    if (timeparts.length == 1) {
                        // 0 or 0-10
                        var hours = timeparts[0].split("-");
                        if (hours.length == 1) {
                            if (hours[0] === "") {
                                start = "0";
                                end = "0";
                            }
                            else {
                                start = hours[0];
                                end = Number(hours[0])+1;
                            }
                        } else {
                            start = hours[0];
                            end = Number(hours[1])+1;
                        }
                    } else {
                        // 23,0 or 17-23,0-10 or 23,0-2 or 17-23,0
                        var startparts = timeparts[0].split("-");
                        start = startparts[0];

                        var endparts = timeparts[1].split("-");
                        if (endparts.length == 1) {
                            end = Number(endparts[0])+1;
                        } else {
                            end = Number(endparts[1])+1;
                        }
                    }
                    $("#inject-time-interval-time-end").val(end);
                    $("#inject-time-interval-time-start").val(start);

                }
            } else {
                $("#inject-time-type-select").val("none");
            }

            $(".inject-time-row").hide();
            $("#inject-time-type-select").val(repeattype);
            $("#inject-time-row-"+repeattype).show();

            /* */

            $('#node-inject-test-inject-button').css("float", "right").css("margin-right", "unset");

            if (RED.nodes.subflow(node.z)) {
                $('#node-inject-test-inject-button').attr("disabled",true);
            }

            $("#inject-time-type-select").trigger("change");
            $("#inject-time-interval-time-start").trigger("change");
            // oneditprepare 반복을 위한 스크립트 end
        },
        oneditsave: function() {
            // oneditsave 반복을 위한 스크립트 start
            var repeat = "";
            var crontab = "";
            var type = $("#inject-time-type-select").val();
            if (type == "none") {
                // nothing
            } else if (type == "interval") {
                var count = $("#inject-time-interval-count").val();
                var units = $("#inject-time-interval-units").val();
                if (units == "s") {
                    repeat = count;
                } else {
                    if (units == "m") {
                        //crontab = "*/"+count+" * * * "+days;
                        repeat = count * 60;
                    } else if (units == "h") {
                        //crontab = "0 */"+count+" * * "+days;
                        repeat = count * 60 * 60;
                    }
                }
            } else if (type == "interval-time") {
                repeat = "";
                var count = $("#inject-time-interval-time-units").val();
                var startTime = Number($("#inject-time-interval-time-start").val());
                var endTime = Number($("#inject-time-interval-time-end").val());
                var days = $('#inject-time-interval-time-days input[type=checkbox]:checked').map(function(_, el) {
                    return $(el).val()
                }).get();
                if (days.length == 0) {
                    crontab = "";
                } else {
                    if (days.length == 7) {
                        days="*";
                    } else {
                        days = days.join(",");
                    }
                    var timerange = "";
                    if (endTime == 0) {
                        timerange = startTime+"-23";
                    } else if (startTime+1 < endTime) {
                        timerange = startTime+"-"+(endTime-1);
                    } else if (startTime+1 == endTime) {
                        timerange = startTime;
                    } else {
                        var startpart = "";
                        var endpart = "";
                        if (startTime == 23) {
                            startpart = "23";
                        } else {
                            startpart = startTime+"-23";
                        }
                        if (endTime == 1) {
                            endpart = "0";
                        } else {
                            endpart = "0-"+(endTime-1);
                        }
                        timerange = startpart+","+endpart;
                    }
                    if (count === "0") {
                        crontab = count+" "+timerange+" * * "+days;
                    } else {
                        crontab = "*/"+count+" "+timerange+" * * "+days;
                    }
                }
            } else if (type == "time") {
                var time = $("#inject-time-time").val();
                var days = $('#inject-time-time-days  input[type=checkbox]:checked').map(function(_, el) {
                    return $(el).val()
                }).get();
                if (days.length == 0) {
                    crontab = "";
                } else {
                    if (days.length == 7) {
                        days="*";
                    } else {
                        days = days.join(",");
                    }
                    var parts = time.split(":");
                    if (parts.length === 2) {
                        repeat = "";
                        parts[1] = ("00" + (parseInt(parts[1]) % 60)).substr(-2);
                        parts[0] = ("00" + (parseInt(parts[0]) % 24)).substr(-2);
                        crontab = parts[1]+" "+parts[0]+" * * "+days;
                    }
                    else { crontab = ""; }
                }
            }

            $("#node-input-repeat").val(repeat);
            $("#node-input-crontab").val(crontab);

            // oneditsave 반복을 위한 스크립트 end
        },
        button: {
            onclick: function(){
                $.ajax({
                    url: "inject/" + this.id,
                    type: "POST",
                    contentType: "application/json; charset=utf-8",
                    success: function (resp) {
                        console.log('inject success : ', resp)
                    },
                    error: function (jqXHR, textStatus, errorThrown) {
                        console.log('inject error : ', textStatus, errorThrown)
                    }
                });
            }
        }
    });
</script>

 

각 주석으로 포함된 부분들이, 반복을 위해 필요한 부분이다.

소스는 위에서 설명한 Node-Red의 기본 노드인 Inject 노드로부터 가져왔다.

 

js 코드

더보기
module.exports = function(RED) {
    "use strict";
    const got = require("got");
    const {scheduleTask} = require("cronosjs");

    function TestNode(config) {
        RED.nodes.createNode(this,config);

        this.repeat = config.repeat;
        this.crontab = config.crontab;
        this.once = config.once;
        this.onceDelay = (config.onceDelay || 0.1) * 1000;
        this.interval_id = null;
        this.cronjob = null;
        var node = this;

        if (node.repeat > 2147483) {
            node.error(RED._("inject.errors.toolong", this));
            delete node.repeat;
        }

        node.repeaterSetup = function () {
            if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) {
                this.repeat = this.repeat * 1000;
                this.debug(RED._("inject.repeat", this));
                this.interval_id = setInterval(function() {
                    node.emit("input", {});
                }, this.repeat);
            } else if (this.crontab) {
                this.debug(RED._("inject.crontab", this));
                this.cronjob = scheduleTask(this.crontab,() => { node.emit("input", {})});
            }
        };

        if (this.once) {
            this.onceTimeout = setTimeout( function() {
                node.emit("input",{});
                node.repeaterSetup();
            }, this.onceDelay);
        } else {
            node.repeaterSetup();
        }

        this.on('input', (msg, send, done) => {
            msg.config = config
            this.send(msg)
            if (done){
                done();
            }
        })
    }

    RED.nodes.registerType("test-node",TestNode);

    TestNode.prototype.close = function() {
        if (this.onceTimeout) {
            clearTimeout(this.onceTimeout);
        }
        if (this.interval_id != null) {
            clearInterval(this.interval_id);
        } else if (this.cronjob != null) {
            this.cronjob.stop();
            delete this.cronjob;
        }
    };

    RED.httpAdmin.post("/inject/:id", RED.auth.needsPermission("inject.write"), function(req,res) {
        var node = RED.nodes.getNode(req.params.id);
        if (node != null) {
            try {
                if (req.body && req.body.__user_inject_props__) {
                    node.receive(req.body);
                } else {
                    node.receive();
                }
                res.sendStatus(200);
            } catch(err) {
                res.sendStatus(500);
                node.error(RED._("inject.failed",{error:err.toString()}));
            }
        } else {
            res.sendStatus(404);
        }
    });
}

 

 

이 노드를 등록하여 확인해 보면 다음 사진과 같이 반복을 위한 노드를 얻을 수 있다.

해당 반복을 설정하게 되면, 아래와 같은 결과를 얻을 수 있다.

1분마다 반복 실행되는 노드.

 

이번 포스팅에서는 Node-Red에서 플로우를 어떻게 실행시키는지에 대해 설명하였다.