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
使い方はこんな感じです。
'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ページを作成しました。
単純なので、そのまま張り付けておきます。
'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 );
<!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に組み込まれているため、そのまま使えます。
#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という文字列が自動的についていました。
#終わりに
以下の投稿を参考にさせていただきました。(ありがとうございました)
m5stackのIPアドレス管理をmDNSに丸投げする
Google HomeのIPアドレスが知りたい!
以上