LoginSignup
0
3

More than 1 year has passed since last update.

Node.jsからmDNSでESP32を検索

Last updated at Posted at 2021-09-20

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

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

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

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

poruruba/mDNS_Test

mDNSを触ってみる

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

> apt-get install avahi-utils

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

> 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) です。
サービスタイプは以下から持ってきているんでしょうか。ですが、自分の好きな文字列でも登録できます。

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

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

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」というエンドポイントを実装しているため、少々見にくくなっていますが、以下のあたりがポイントです。

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行目あたりです。

【変更前】

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

【変更後】

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ページを作成しました。
単純なので、そのまま張り付けておきます。

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 );
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に組み込まれているため、そのまま使えます。

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 といった感じです。

MDNS.begin(MDNS_NAME);

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

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

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

試してみる。

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

image.png

終わりに

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

 m5stackのIPアドレス管理をmDNSに丸投げする
 Google HomeのIPアドレスが知りたい!

以上

0
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3