Node.jsからmDNSでESP32を検索

mDNSがどんなものか実際に触ってみたかったことと、ESP32のIPアドレス管理を楽にしたく、Node.jsからmDNSでESP32を検索してみます。

「XXXX.local」という名前のホスト名を探したことがあるかもしれません。mDNSという機能のようです。
DNSに登録していないのに、なぜか名前解決しているので不思議に思っていましたが、ローカルネットワーク用の簡易なDNSサービスのような感じだと思っています。

* ESP32で提供するサービス情報をmDNSに登録します。
* Node.jsからmDNSでサービスを検索します。
* ブラウザから気軽に検索できるようにWebページを作成します。

ソースコードもろもろは以下に置いておきます。

poruruba/mDNS_Test

https://github.com/poruruba/mDNS_Test

#mDNSを触ってみる

Ubuntuを使っている場合は、「avahi-utils」というコマンド群を使って体験できます。

```
> apt-get install avahi-utils
```

以下のコマンドを入力すると、ローカルネットワークにあるｍDNSに登録済みのサービスの情報が表示されます。

```
> avahi-browse -at
+   eno1 IPv4 synology                                      Microsoft Windows Network local
+   eno1 IPv4 m5stickc                                      _testserver._tcp     local
+   eno1 IPv4 qnap(AFP)                                Apple File Sharing   local
+   eno1 IPv4 qnap(SSH)                                SSH Remote Terminal  local
+   eno1 IPv4 Google-Nest-Hub-XXXXXX _googlecast._tcp     local
+   eno1 IPv4 qnap                                     Web Site             local
+   eno1 IPv4 Homebridge XXXX XXXX                          _hap._tcp            local
```

ご丁寧に、一部のサービス名を読みやすい形に変換してくれていたので、今度は素のサービスタイプを以下で表示します。

```
> avahi-utils -atk
+   eno1 IPv4 synology                                      _smb._tcp local
+   eno1 IPv4 m5stickc                                      _testserver._tcp     local
+   eno1 IPv4 qnap(AFP)                                _afpovertcp._tcp   local
+   eno1 IPv4 qnap(SSH)                                _ssh._tcp  local
+   eno1 IPv4 Google-Nest-Hub-XXXXXX _googlecast._tcp     local
+   eno1 IPv4 qnap                                     _http._tcp             local
+   eno1 IPv4 Homebridge XXXX XXXX                          _hap._tcp            local
```

```_XXXX._YYY``` となっていますが、XXXXがサービスタイプ、YYYがプロトコル(TCP or UDP) です。
サービスタイプは以下から持ってきているんでしょうか。ですが、自分の好きな文字列でも登録できます。

http://www.dns-sd.org/servicetypes.html

Googleのスマートスピーカを自分の部屋に置いているので、googlecastで見つかっています。

詳細を見たい場合は以下を実行します。

```
> avahi-browse -rk _googlecast._tcp
+   eno1 IPv4 Google-Nest-Hub-XXXXXX _googlecast._tcp     local
=   eno1 IPv4 Google-Nest-Hub-XXXXXX _googlecast._tcp     local
   hostname = [XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.local]
   address = [192.168.1.XXX]
   port = [8009]
   txt = ["rs=" "nf=1" "bs=XXXXXXXX" "st=0" "ca=XXXXXX" "fn=マイネストハブ" "ic=/setup/icon.png" "md=Google Nest Hub" "ve=05" "rm=" "cd=XXXXXXXXXXXXXXXXXXXXXXX" "id=XXXXXXXXXXXXXXXXXXX"]
```

#Node.jsからmDNSでサービスを検索します。

以下のnpmモジュールを使わせていただきました。

agnat/node_mdns

https://github.com/agnat/node_mdns

使い方はこんな感じです。

```js:node.js\api\controllers\mdns-api\index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');

const mdns = require('mdns');

exports.handler = async (event, context, callback) => {
	if( event.path == '/mdns-list'){
		var body = JSON.parse(event.body);
		console.log(body);
		var timeout = !body.timeout ? 3000 : body.timeout;
		var protocol = !body.protocol ? 'tcp' : body.protocol;

		var serviceType;
		if( protocol == 'tcp')
			serviceType = mdns.tcp(body.servicetype);
		else if( protocol == 'udp' )
			serviceType = mdns.udp(body.servicetype);
		else
			throw 'unknown protocol';

		const browser = mdns.createBrowser(serviceType);

		var list = [];
		browser.on('serviceUp', (service) =>{
			console.log(service);
			var item = {
				name: service.name,
				address: service.addresses,
				port: service.port,
				host: service.host,
				fullname: service.fullname,
				servicetype: service.type.name,
				protocol: service.type.protocol,
			};
			list.push(item);
		});

		browser.start();

		return new Promise((resolve, reject) =>{
			setTimeout(() =>{
				browser.stop();
				resolve(new Response({ list: list }));
			}, timeout);
		});
	}
};
```

expressで、「/mdns-list」というエンドポイントを実装しているため、少々見にくくなっていますが、以下のあたりがポイントです。

```js
const mdns = require('mdns');
・・・
const browser = mdns.createBrowser(serviceType);
・・・
browser.on('serviceUp', (service) =>{
・・・
browser.start();
・・・
```

serviceTypeで、検索条件(サービスタイプ名、プロトコル)を指定しています。


〇Linuxの場合の重要事項

Windowsでは問題ないのですが、Linuxでnpmモジュール「mdns」を使う場合には、mdnsのソースの修正が必要です。

```node_modules/mdns/lib/browser.js``` の121行目あたりです。

【変更前】

```js
Browser.defaultResolverSequence = [
  rst.DNSServiceResolve()
  , 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()
, rst.makeAddressesUnique()
];
```

【変更後】

```js
Browser.defaultResolverSequence = [
  rst.DNSServiceResolve()
  , 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({ families: [0] })
, rst.makeAddressesUnique()
];
```
この修正を入れないと、以下のログをはいてnode.js自体がダウンします。

```
Error: getaddrinfo -3001
    at errnoException (/home/XXXX/projects/node/public_sites/node_modules/mdns/lib/resolver_sequence_tasks.js:199:11)
    at getaddrinfo_complete (/home/XXXX/projects/node/public_sites/node_modules/mdns/lib/resolver_sequence_tasks.js:112:10)
    at GetAddrInfoReqWrap.oncomplete (/home/XXXX/projects/node/public_sites/node_modules/mdns/lib/resolver_sequence_tasks.js:120:9) {
  code: -3001,
  errno: -3001,
  syscall: 'getaddrinfo'
}
```

また、以下の警告表示がうざかったら、環境変数「AVAHI_COMPAT_NOWARN=1」で抑止できます。

```
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see http://0pointer.de/blog/projects/avahi-compat.html
```

#ブラウザから気軽に検索できるようにWebページを作成します。

さきほどは、npmのmdnsの使い方を示しながら、WebAPIのエンドポイントを立ち上げていました。
今度は、JavascriptからWebAPIを呼び出して、表示しているだけの、単純な静的Webページを作成しました。
単純なので、そのまま張り付けておきます。

```js:node.js\public\mdns\js\start.js
'use strict';

//const vConsole = new VConsole();
//window.datgui = new dat.GUI();

const base_url = "【立ち上げたエンドポイントのURL】";

var vue_options = {
    el: "#top",
    mixins: [mixins_bootstrap],
    data: {
        timeout: 3000,
        servicetype: 'http',
        protocol: 'tcp',
        service_list: [],
        servicetype_list: [
            'http', "smb", "ssh", "googlecast", "hap"
        ],
    },
    computed: {
    },
    methods: {
        servicetype_select: function(index){
            this.servicetype = this.servicetype_list[index];
        },
        do_browse: async function(){
            try{
                this.progress_open();
                var param = {
                    servicetype: this.servicetype,
                    timeout: this.timeout,
                    protocol: this.protocol
                };
                var json = await do_post(base_url + '/mdns-list', param );
                console.log(json);
                this.service_list = json.list;
            }catch(error){
                console.error(error);
                alert(error);
            }finally{
                this.progress_close();
            }
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog
vue_add_global_components(components_bootstrap);
vue_add_global_components(components_utils);

/* add additional components */
  
window.vue = new Vue( vue_options );
```

```html:node.js\public\mdns\index.js
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <link rel="stylesheet" href="css/start.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/spinkit/2.0.1/spinkit.min.css" />
  <script src="js/methods_bootstrap.js"></script>
  <script src="js/components_bootstrap.js"></script>
  <script src="js/components_utils.js"></script>
  <script src="js/vue_utils.js"></script>
  <script src="js/gql_utils.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/vconsole/dist/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>

  <title>mDNSブラウザ</title>
</head>
<body>
<!--
    <div id="loader-background">
      <div class="sk-plane sk-center"></div>
    </div>
-->
    <div id="top" class="container">
        <h1>mDNSブラウザ</h1>

        <div class="form-inline">
          <button class="btn btn-primary btn-lg" v-on:click="do_browse">検索</button>
          <label>servicetype</label>
          <div class="dropdown input-group">
            <input class="form-control" type="text" data-toggle="dropdown" v-model="servicetype">
            <ul class="dropdown-menu">
              <li v-for="(item, index) in servicetype_list" value="item" v-on:click="servicetype_select(index)"><a
                  href="#">{{item}}</a></li>
            </ul>
          </div>
          <label>protocol</label> <select class="form-control" v-model="protocol">
            <option value="tcp">tcp</option>
            <option value="udp">udp</option>
          </select>
          <label>timeout</label> <input type="number" class="form-control" v-model.number="timeout">
        </div>

        <hr>

        <p>
          <label>num_of_service</label> {{service_list.length}}
        </p>
        <div v-for="(item, index) in service_list">
          <div class="panel panel-default">
            <div class="panel-heading">
              <h5>{{item.name}}</h5>
            </div>
            <div class="panel-body">
              <label>name</label> {{item.name}}<br>
              <label>host</label> {{item.host}}<br>
              <label>servicetype</label> {{item.servicetype}}<br>
              <label>address</label> {{item.address }}<br>
              <label>port</label> {{item.port }}<br>
              <label>protocol</label> {{item.protocol}}<br>
              <label>fullname</label> {{item.fullname}}<br>
            </div>
          </div>
        </div>
        <br>
        

        <!-- for progress-dialog -->
        <progress-dialog v-bind:title="progress_title"></progress-dialog>
    </div>

    <script src="js/start.js"></script>
</body>
```



# ESP32で提供するサービス情報をmDNSに登録します

mDNSの実装はESP-IFに組み込まれているため、そのまま使えます。

```cpp:Arduino\mDNS_test\src\main.cpp
#include <M5StickC.h>
#include <WiFi.h>
#include <ESPmDNS.h>

const char *wifi_ssid = "【WiFiアクセスポイントのSSID】";           // WiFiアクセスポイントのSSID
const char *wifi_password = "【WiFiアクセスポイントのパスワード】"; // WiFiアクセスポイントのパスワード
const char *MDNS_NAME = "m5stickc";

void wifi_connect(const char *ssid, const char *password);

void setup() {
  M5.begin();

  wifi_connect(wifi_ssid, wifi_password);

  if (!MDNS.begin(MDNS_NAME)){
    Serial.print("Error MDNS_NAME:");
    Serial.println(MDNS_NAME);
    while(1);
  }

  MDNS.addService("testserver", "tcp", 80);
}

void loop() {
  M5.update();
}

void wifi_connect(const char *ssid, const char *password)
{
  Serial.println("");
  Serial.print("WiFi Connenting");

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED){
    Serial.print(".");
    delay(1000);
  }
  Serial.println("Connected");
  Serial.println(WiFi.localIP());
}
```

以下の部分で、指定した名前(MDNS_NAME)が以降のmDNSに関する処理で使われます。
また、PCからPINGでホスト名を指定したり、ブラウザからURLで指定するときに、```MDNS_NAME.local``` という名前が使えるようになります。今回の場合は、```m5stickc.local``` といった感じです。

```cpp
MDNS.begin(MDNS_NAME);
```

以下の部分が、mDNSに自身をサービス登録する部分です。各引数は、サービスタイプ名、プロトコル、ポート番号です。

```cpp
MDNS.addService("testserver", "tcp", 80);
```

既に定義されたサービスタイプ名でもいいですし、自分の好きな名称でもよいです。
ちなみに、この実装を見てわかるように、サービスを登録したからと言って、そのサービスが提供されているとは限りません。あくまで、サービスの情報を登録しているのみです。

#試してみる。

静的Webページをブラウザで開き、servicetypeにtestserverを指定して検索ボタンを押してみましょう。ESP32が検出されました。
今回は、2台のESP32を起動した状態で実行してみました。その場合、MDNS_NAMEで指定した名前がかぶってしまうのですが、-2という文字列が自動的についていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/626a3d16-924c-175a-0252-467188eab3bb.png) 

#終わりに

以下の投稿を参考にさせていただきました。（ありがとうございました）

　[m5stackのIPアドレス管理をmDNSに丸投げする](https://qiita.com/yasunori_oi/items/3a262a923f1aeea0f50c)
　[Google HomeのIPアドレスが知りたい！](https://uepon.hatenadiary.com/entry/2021/05/05/231603)


以上