The Moment

阿宅的筆記

Creating Flow in Node Red - 3

先前建了兩個 node-red 簡單的 flow ,現在試著做做正事。

常見的 IoT 應用通常都是 event/trigger 的型式,正好最近當紅的 pokemon 的怪物偵測符合

if pokemon spawn then trigger notification

這樣的條件,那就試著用 Node Red 來玩玩看好了

Creating pokemon detecting function node


偵測 pokemon 出現與否當然不會是 Node Red 內建的 Node ,好在現在網路上的 Open source 很多,我們只要找一個能用的來把它包成能運作的 Node 就好,仿照 Creating Flow In Node Red - 2 裡介紹的方法建立自定的 node , 以下是執行的步驟

  • 在 ~/.node-red/nodes 下建立目錄 node-red-node-pokemon
  • 執行 npm init 建立 package.json 並做適當的編輯 (加入node-red 屬性)
    package.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "name": "node-rede-note-pokemon",
    "version": "1.0.0",
    "description": "Node Red node to show pokemon ",
    "main": "pokemon.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [
    "node-red",
    "pokemon"
    ],
    "node-red": {
    "nodes": {
    "pokemon": "pokemon.js"
    }
    },
    "author": "",
    "license": "ISC"
    }
  • 執行 npm install pokemon-go-node-api --save 安裝人家寫好的 pokemon 偵測模組
  • 編輯 pokemon.js,其中用戶名稱、密碼、經緯座標等四個值是由 Node Red 的 form 放在 config 送出來,稍後鄉 pokemon.html 中會看到對應的輸入欄位,其他部份基本上是依 pokemon go node api 的範例做簡單的改寫而已,有興趣了解這個 API 的可以到 Github上研究
    pokemon.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    module.exports = function(RED) {
    function PokemonNode(config) {
    RED.nodes.createNode(this,config);
    var node = this;
    this.on('input', function(msg) {
    node.log('pokemon go on input');
    var a = require('pokemon-go-node-api');
    var location = {
    type: 'coords',
    coords : {
    latitude: config.lat,
    longitude: config.lng,
    altitude: 1
    }
    };
    msg.pokemons = [];
    var username = process.env.PGO_USERNAME || config.username;
    var password = process.env.PGO_PASSWORD || config.password;
    var provider = process.env.PGO_PROVIDER || 'google';
    a.init(username, password, location, provider, function(err) {
    if (err) {
    node.log( err );
    return;
    }
    node.log('1[i] Current location: ' + a.playerInfo.locationName);
    node.log('1[i] lat/long/alt: : ' + a.playerInfo.latitude + ' ' + a.playerInfo.longitude + ' ' + a.playerInfo.altitude);
    a.GetProfile(function(err, profile) {
    if (err) {
    node.log( "Get profile error : " + err ) ;
    return ;
    }
    a.Heartbeat(function(err,hb) {
    if (err) {
    node.log( "Heart beat error : " + err );
    return;
    }
    // get near by pokemon
    for (var i = hb.cells.length - 1; i >= 0; i--) {
    if(hb.cells[i].NearbyPokemon[0]) {
    var pokemon = a.pokemonlist[parseInt(hb.cells[i].NearbyPokemon[0].PokedexNumber)-1];
    node.log('1[+] There is a ' + pokemon.name + ' near.');
    msg.pokemons.push( "There is a " + pokemon.name + " near.");
    }
    }
    // get spawned pokemon
    for (i = hb.cells.length - 1; i >= 0; i--) {
    for (var j = hb.cells[i].MapPokemon.length - 1; j >= 0; j--) {
    var currentPokemon = hb.cells[i].MapPokemon[j];
    (function(currentPokemon) {
    var pokedexInfo = a.pokemonlist[parseInt(currentPokemon.PokedexTypeId)-1];
    node.log('[+] There is a ' + pokedexInfo.name + ' near!! U can try to catch it!');
    msg.pokemons.push( "There is a " + pokedexInfo.name +" U can try to catch.");
    })(currentPokemon);
    }
    }
    });
    });
    });
    node.send(msg);
    });
    }
    RED.nodes.registerType("pokemon",PokemonNode);
  • 編輯 pokemon.html,第一個 script 區塊中,default 屬性要把要讓使用者在 Node-red 中填入的數值放進來,如果有預設值,可以指定給value屬性,這邊如果不加, Node Red 中會有欄位可以填,但數值無法透過 config 變數帶到 pokemon.js 去。第二個 script 區塊則是指定之後在 Node Red 中的輸入欄位 (HTML)
    pokemon.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    <script type="text/javascript">
    RED.nodes.registerType('pokemon',{
    category: 'function',
    color: '#a6bbcf',
    defaults: {
    name: {value:"Pokemon"},
    username: {value: "" },
    password: { value: "" },
    lat: {value : "25.036922"},
    lng: {value : "121.523681"},
    alt: {value : "0"}
    },
    inputs:1,
    outputs:1,
    icon: "file.png",
    label: function() {
    return this.name||"pokemon";
    }
    });
    </script>
    <script type="text/x-red" data-template-name="pokemon">
    <div class="form-row">
    <label for="node-input-name"><i class="icon-tag"></i> Name</label>
    <input type="text" id="node-input-name" placeholder="Name">
    </div>
    <div class="form-row">
    <label for="node-input-username"><i class="icon-tag"></i> Username</label>
    <input type="text" id="node-input-username" placeholder="Google mail">
    </div>
    <div class="form-row">
    <label for="node-input-password"><i class="icon-tag"></i> Password</label>
    <input type="password" id="node-input-password" placeholder="Password">
    </div>
    <div class="form-row">
    <label for="node-input-lat"><i class="icon-tag"></i> 緯度</label>
    <input type="text" id="node-input-lat" placeholder="Latitude">
    </div>
    <div class="form-row">
    <label for="node-input-lng"><i class="icon-tag"></i> 經度</label>
    <input type="lng" id="node-input-lng" placeholder="Longitude">
    </div>
    <div class="form-row">
    <label for="node-input-alt"><i class="icon-tag"></i> 海拔</label>
    <input type="lng" id="node-input-alt" placeholder="Altitude">
    </div>
    </script>
    <script type="text/x-red" data-help-name="pokemon">
    <p>A simple node that shows nearby pokemon</p>
    </script>

Creating Notification output node


能偵測 pokemon 後,除了在 console log 中看到訊息外,還希望能直接通知使用者,這個部份 Node Red 內建了 Email / Twitter 兩種方式,但這樣感覺不夠及時,沒開 Mail、網頁就看不到了,最好是直接在桌面上跳出提示訊息,這意謂著,又要客製了 XD

好在這同樣有人家寫好的Library 可以用 (Open Source 萬歲!!!),那就依樣畫葫蘆再建一個自訂的 Node 吧。

  • ~/.node-red/nodes 下建立 node-red-node-notifier 目錄
  • 執行 npm init 建立 package.json 並做適當的編輯 (加入node-red 屬性)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    "name": "node-red-node-notifier",
    "version": "1.0.0",
    "description": "",
    "main": "notifier.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [
    "node-red",
    "notifier"
    ],
    "author": "",
    "license": "ISC",
    "dependencies": {
    "node-notifier": "^4.6.1"
    },
    "node-red": {
    "nodes": {
    "notifier": "notifier.js"
    }
    }
    }
  • 編輯 notifier.js
    notifier.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    module.exports = function(RED) {
    function NotifierNode(config) {
    RED.nodes.createNode(this,config);
    var node = this;
    this.on('input', function(msg) {
    if (msg.pokemons.length > 0) {
    const notifier = require('node-notifier');
    var alarm_msg = ""
    for (var i in msg.pokemons ) {
    alarm_msg += msg.pokemons[i] + "\n";
    }
    notifier.notify ({
    "title": "Pokemon !!",
    "message": alarm_msg
    });
    }
    });
    }
    RED.nodes.registerType("notifier",NotifierNode);
    }
  • 編輯 notifier.html ,這個 node 的 category 記得要改成 output , 並把 output 改為 0
    notifier.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <script type="text/javascript">
    RED.nodes.registerType('notifier',{
    category: 'output',
    color: '#a6bbcf',
    defaults: {
    name: {value:"Mac Notifier"}
    },
    inputs:1,
    outputs:0,
    icon: "file.png",
    label: function() {
    return this.name||"Mac Notifier";
    },
    align: "right"
    });
    </script>
    <script type="text/x-red" data-template-name="notifier">
    <div class="form-row">
    <label for="node-input-name"><i class="icon-tag"></i> Name</label>
    <input type="text" id="node-input-name" placeholder="Name">
    </div>
    </script>
    <script type="text/x-red" data-help-name="notifier">
    <p>A simple node that shows notifications</p>
    </script>

Creating flow


好了,現在需要的 node 都有了,接著只要把流程串好就好了,長得像下圖這樣 flow-3.png

很直覺,對吧 ?!

不,一點也不,為什麼在 notifier 前要加上 兩秒的 delay ?!!!

熟悉 javascript 開發的朋友應該猜得到,又是非同步的問題,在上面的流程中,pokemon node 去抓 pokemon 資料的同時,流程已經走到 notifier 了,這個時候資料還沒準備好,所以值都會是空的,用寫程式的方法可以 callback 方式處理,但在流程圖中,一時想不到什麼好解法(再弄一個 event trigger 的 flow 接資料 ? XD),就硬塞一個 delay node 假裝沒這件事,實際上,並沒有解決這個問題,但至少至少,可以看到期望的結果了。

其中 Inject Node 設定如下圖,除了一開始執行的選項外,還設定每隔十秒重新執行一次 Inject Node Setting

Pokemon Node 的設定如下圖,程式中需要的參數都在這邊設定,再帶進 pokemon.js 裡 Pokemon Node Setting

Deploy 看看吧 !!! 萬歲,我身邊有一隻波波可以抓 orz Mac Notification Result

這樣搞完後發現,好像也不那麼像 IoT 的流程,加上那個不同步的問題,嗯嗯… Node Red 有 MQTT 的 Input/Output Node ,晚一點用它來試試看好了@@

另一方面,在商業邏輯複雜到一定程度後, Node Red 內建的 Node 很明顯的無法全部支援到,勢必要自己寫 Node, 但這樣一來… 我幹麻不直接都在程式裡幹完就好了 XD XD 若說真要省事,就要有人家寫好的 Node library 支援,只是很可惜的,小公司為了十萬塊把捧著 device connecting node 上門找合作的 A 公司趕走惹,未來似乎只能科科,科科 (嘆