VPS に Mosquitto をインストールしてブラウザと IoT デバイス間で双方向通信を行う (3)

Web ブラウザ ソフトウエアコード

MQTT Mosquitto ブラウザ Web ソケット WebSocket IoTデバイス 監視・制御 SSL/TLS

背景

MQTT プロトコルは IoT 機器の監視・制御に最適な通信方式です。 Mosquitto や EMQX社 から無料で利用できる MQTT Broker が提供されていて手軽に利用できます。 しかし予期せぬサーバーの停止により IoT 機器の監視・制御に不都合をきたしたり、相乗りサーバーのためどうしてもセキュリティに対する不安も残ります。

これらの課題を解決するために VPS サーバーにセキュリティ対策も行った MQTT Broker をインストールして IoT システムを構成しました。 特に Web ブラウザと MQTT Broker 間の MQTT over WebSockets with TLS に多くの試行錯誤をともなったので整理して記録を残します。

ゴール

図 1 Web ブラウザ IoT デバイス 間 MQTT 通信

・VPS サーバーに MQTT Broker インストール
・TLS(SSL) によるセキュリティ対応
・IoT デバイス (C++) と MQTT Broker 間は MQTT over TLS (mqtts://) 通信
・Web ブラウザ (HTML, Java Script, CSS) と MQTT Broker 間 MQTT over WebSockets with TLS (wss://) 通信

本章では HTML, Java Script, CSS でコードを作成して Web ブラウザと MQTT Broker 間で MQTT over WebSockets with TLS (wss://) 通信を行います。

前提条件

・Mosquitto (バージョン 2.0.11) インストール済み
・VPSサーバー Linux OS Debian (バージョン 6.1.153-1) 
・IoT デバイス ESP32-DevKitC
・開発プラットフォーム Visual Studio Code (バージョン:1.105.0)
・拡張機能 PlatformIO IDE for VSCode (バージョン 3.3.4)
・開発言語 Linux , C++, HTML, Java Script, CSS
・通信プロトコル MQTT over TLS (mqtts://), MQTT over WebSockets with TLS (wss://)
・Windows 11 (バージョン Pro 24H2)

概要

図 2 ブラウザ画面

図 2 ブラウザの画面はスマートフォン、PC どちらでも利用できる設計です。

・現在時刻     ISO 8601 に準じて表示します。
・ON、OFF ボタン クリックすると ON または OFF の制御情報をパブリッシュします。
・クライアント ID MQTT Broker に正常に接続できたらクライアント ID を表示します。
          クライアント ID はブラウザ起動毎に異なる値をとります。
・published 窓  パブリッシュしたメッセージを表示します。
・subscribeed 窓 サブスクライブしたメッセージを表示します。

HTML コード

図 3 に HTML コード全文を示します。右上隅にカーソルを合わせると一括コピーできます。

<!-- MQTT over WebSocket with TLS -->
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="MQTT_VPS.css">
    <title>MQTT_VPS_HTML</title>
    <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
    <script src="MQTT_VPS.js"></script>
  </head>
  <p id="msg_time">time</p>
  <div class="inline_1">   
    <button class="bt_on" onclick="bt_onClicked(this)" id="bt_on">ON</button>
    <button class="bt_off" onclick="bt_offClicked(this)" id="bt_off">OFF</button>
  </div>
  <br>
  <div class="inline_2"><p></p><br>
    <div name="msg_mqtt" id="msg_mqtt">MQTT メッセージ</div><br>
  </div>
  <div name="msg_pub_ttl" id="msg_pub_ttl">published</div>
  <div class= "inline_3">
    <textarea name="msg_pub" id="msg_pub">publish メッセージ:</textarea></p>
  </div> 
  <div name="msg_sub_ttl" id="msg_sub_ttl">subscribed</div>
  <div class= "inline_4">
    <textarea name="msg_sub" id="msg_sub">subscribe メッセージ:</textarea></p>
  </div>
</html>
図 3 HTML コード

7 行目 画面レイアウトはスタイルシート MQTT_VPS.css で指定します。
7  <link rel=”stylesheet” href=”MQTT_VPS.css”>

9 行目 CDN (Content Delivery Network)を使い MQTT クライアントライブラリをインポートします。
9 <script src=”https://unpkg.com/mqtt/dist/mqtt.min.js”>

10 行目 Java Script ファイル MQTT_VPS.js (後述)を読み込みます。
     ( MQTT Broker へ接続して IoT デバイスを監視・制御するコード )
10 <script src=”MQTT_VPS.js”></script> </script>

画面に配置する要素には class、id を付加して Java Script、CSS から紐づけしています。

Java Script コード

図 4 に Java Script コード全文を示します。右上隅にカーソルを合わせると一括コピーできます。

// MQTT_VPS.js
// MQTT over WebSocket with TLS

const clientId = "BROWSER_" + Math.random().toString(16).substring(2, 8)
const connectUrl = "wss://domain-name:8884/mqtt";
var caFile;

//CA ファイル読み込み 
fetch('./ISRG_Root_X1.pem')
  .then(response => response.text())
  .then(text => {
   caFile = text;
   console.log(caFile);
});

//MQTT option, topic, qos パラメータ設定
const options = {
  keepalive: 60,
  clientId: clientId,
  clean: true,
  connectTimeout: 30 * 1000,
  username: "username",
  password: "password",
  reconnectPeriod: 1000,
  rejectUnauthorized: true, 
  ca: caFile,
  path: '/mqtt',
}
const topic = "MQTT topic";
const qos = 0;

var date2 = "";
var payload = "WebSocket mqtt test" //payload 初期値
var msg_subscribe = "";           //subscribe したメッセージ全文
var msg_publish = "";       //publish するメッセージ初期値

//接続準備
console.log("connecting mqtt client")
const client = mqtt.connect(connectUrl, options)

//接続エラー処理
client.on("error", (err) => {
  console.log("Connection error: ", err)
  document.getElementById("msg_mqtt").innerHTML = "Connection error: ", err
  client.end()
  window.location.reload();
})

//再接続処理
client.on("reconnect", () => {
  console.log("Reconnecting...")
  document.getElementById("msg_mqtt").innerHTML = "Reconnecting..."
})

//接続処理
client.on("connect", () => {
  console.log("Client connected:" + clientId)
  document.getElementById("msg_mqtt").innerHTML = "Client connected:" + clientId
})

//メッセージをサブスクライブして画面に表示する
//   https://github.com/mqttjs/MQTT.js#event-message
client.on("message", (topic, payload) => { 
    //payload を String 形式に変換して msg_subscribe に代入する
    msg_subscribe = payload.toString();
    document.getElementById("msg_sub").innerHTML = msg_subscribe;
    console.log("Received Message: "+msg_subscribe+"\nOn topic: "+topic );
  }
)

//Web ページ(全リソース)が完全に読み込まれたら実行する関数
window.onload = function () {
  sub_msg();  //サブスクライブする関数の呼び出し 
  get_time(); //時刻取得
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
//                       ここから繰り返し処理                                                      //
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
var repeat_duration = 1000;
  setInterval(function(){
  get_time();
},repeat_duration);
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//

//--------------------- ここから ボタンbt_offをクリックした時の処理 -----------------------------//
function bt_offClicked(elment){
  const buf = "{\"date\":\""+date2+"\",\"client\":\"BROWSER\",\"state\":\"NULL\",\"control\":\"OFF_\"}"
  pub_msg(buf);    
}

//---------------------- ここから ボタンbt_onをクリックした時の処理 -----------------------------//
function bt_onClicked(element){
  const buf = "{\"date\":\""+date2+"\",\"client\":\"BROWSER\",\"state\":\"NULL\",\"control\":\"ON__\"}"
  pub_msg(buf);
}

//------------------------ ここから メッセージ msg_publish をパブリッシュする関数 -------------------//
function pub_msg(msg_publish){
  payload = msg_publish;
  client.publish(topic, payload, { qos }, (error) => {
    if (error) {
      console.error(error)
    }
  })
  document.getElementById("msg_pub").innerHTML = payload.toString();
  return;
}

//------------------------ ここから メッセージをサブスクライブする関数 -------------------------------//
function sub_msg(){
  client.subscribe(topic, { qos }, (error) => {
    if (error) {
      console.log("Subscribe error:", error)
      return
    }
    console.log(`Subscribe to topic ${topic}`);
  })
  return;
}

//------------------------ ここから 現在時刻を取得する関数 -----------------------------------------//
function get_time(){
  const date1 = new Date();
  date2 = date1.getFullYear() + "-" + 
  ("0"+(date1.getMonth() + 1)).slice(-2)  + "-" + 
  ("0"+(date1.getDate())).slice(-2) + "T" +
  ("0"+(date1.getHours())).slice(-2) + ":" + 
  ("0"+(date1.getMinutes())).slice(-2) + ":" + 
  ("0"+(date1.getSeconds())).slice(-2) + "." +
  String(date1.getMilliseconds()).padStart(3, "0") ;
  //ミリセカンド( 0 ~ 999 整数)は 0 で埋めて 3 桁に揃える
  document.getElementById("msg_time").innerHTML = date2;
  return;
}

図 4 Java Script コード

4 行目 文字列と乱数からユニークな clientId を生成します。
     ( MQTT Broker に複数のクライアントから同じ clientId で接続を試みるとエラーが発生します。)
5 行目 MQTT Broker に接続するための URL をconnectUrl に設定します。
    Mosquitto インストール時に設定した domain-name にポートとパスを付加します。
4 const clientId = “BROWSER_” + Math.random().toString(16).substring(2, 8)
5 const connectUrl = “wss://domain-name:8884/mqtt”;

事前に Mosquitto インストール時に設定した CA 証明書ファイル ( ISRG_Root_X1.pem ) をコピーして html ファイルと同じフォルダに保存しておきます。

9-14 行目 fetch() 関数の引数にフォルダーパス ( ./ ) とファイル名 ( ISRG_Root_X1.pem ) を指定することで証明書ファイルを読み込み、テキストとして 変数 caFile に格納します。
 9 fetch('./ISRG_Root_X1.pem')
10   .then(response => response.text())
11   .then(text => {
12   caFile = text;
13   console.log(caFile);
14 });

MQTT 通信に使用するパラメータを設定します。
18 行目 keepalive: 60;
   MQTT Broker との間で定期的 ( 60 秒) に接続が有効か確認します。
19 行目 clientId: clientId,
   ユニークな clientId (前述) を使用します。
20 行目 clean: true,
   QoS0 のみ使用するので接続毎に新しいセッションを作成します。
21 行目 connectTimeout: 30 * 1000,
   接続タイムアウト時間 ( 30 秒 ) を設定します。
22 行目 username: “username”,
23 行目 password: “password”,
   Mosquitto インストール時のドメイン名、パスワードを設定します。
24 行目 reconnectPeriod: 1000,
   再接続間隔時間 ( 1 秒 )を設定します。
25 行目 rejectUnauthorized: true,
   MQTT Broker から示された証明書が信頼できる認証局 (CA) により署名されているか検証します。
26 行目 ca: caFile,
   読み込んだ CA 証明書 (前述) を設定します。
27 行目 path: ‘/mqtt’,
   WebSocket に必要なパス ( /mqtt ) を設定します。 ( 5 行目と重複かも知れません。)
29 行目 topic = “MQTT topic”;
   ここでは MQTT topic を設定します。
30 行目 qos = 0;
   メッセージを1回だけ送信する QoS=0 に設定します。
   相手に到達したかどうかは確認していません。
17 const options = {
18   keepalive: 60,
19   clientId: clientId,
20   clean: true,
21   connectTimeout: 30 * 1000,
22   username: "username",
23   password: "password",
24   reconnectPeriod: 1000,
25   rejectUnauthorized: true, 
26   ca: caFile,
27   path: '/mqtt',
28 }
29 const topic = "MQTT topic";
30 const qos = 0;

接続準備
38 行目 接続中であることをコンソールにメッセージ出力します。
39 行目 MQTT Broker への接続を確立するための関数を呼び出すことでメッセージの送受信を開始できる状態になります。引数には connectUrl、options (前述) を入力します。
38 console.log(“connecting mqtt client”)
39 const client = mqtt.connect(connectUrl, options)

接続エラー処理
43-44 行目 コンソールとブラウザ画面両方に “Connection error: ” のメッセージとエラー番号を出力します。
45 行目 client.end() により接続を切断します。
46 行目 window.location.reload() 現在のページを再読み込みさせて再接続を試みます。
    (後述の再接続処理があるので不要かもしれません)
42 client.on("error", (err) => {
43   console.log("Connection error: ", err)
44   document.getElementById("msg_mqtt").innerHTML = "Connection error: ", err
45   client.end()
46   window.location.reload();
47 })

再接続処理
50 行目 クライアントライブラリが自動的に再接続を検知した場合再接続処理を行います。
51-52 行目 コンソールとブラウザ画面両方に “Reconnecting…” のメッセージを出力します。
50 client.on("reconnect", () => {
51   console.log("Reconnecting...")
52   document.getElementById("msg_mqtt").innerHTML = "Reconnecting..."
53 })

接続処理
56 行目 MQTTブローカーへの接続が正常に確立されたときに実行されるコールバック関数(イベントハンドラ)を登録します。
57-58 行目 コンソールとブラウザ画面両方に “Client connected:” のメッセージと clientId の内容を出力します。
56 client.on("connect", () => {
57   console.log("Client connected:" + clientId)
58   document.getElementById("msg_mqtt").innerHTML = "Client connected:" + clientId
59 })

メッセージをサブスクライブして画面に表示します。
63 行目 メッセージを受信した際に実行されるコールバック関数を登録します。
     引数を topic にすると戻り値として payload が得られます。
65 行目 payload を String 形式に変換して msg_sbscribe に代入します。
66 行目 msg_subscribe をブラウザ画面に出力します。
67 行目 msg_subscribe と topic の内容をコンソールに出力します。
63 client.on("message", (topic, payload) => { 
64     //payload を String 形式に変換して msg_subscribe に代入する
65     msg_subscribe = payload.toString();
66     document.getElementById("msg_sub").innerHTML = msg_subscribe;
67     console.log("Received Message: "+msg_subscribe+"\nOn topic: "+topic );
68   }
69 )

Web ページ(全リソース)が完全に読み込まれたら実行する関数
72 行目 window.onload = function () {…} により全リソースが読み込まれたら実行する関数を記述します。
73 行目 sub_msg() サブスクライブする関数 (後述) を呼び出します。
74 行目 get_time() 時刻を取得する関数 (後述) を呼び出します。
72 window.onload = function () {
73   sub_msg();  //サブスクライブする関数の呼び出し
74   get_time(); //時刻取得
75 }

繰り返し処理
80 行目 変数 repeat_duration 繰り返し周期を 1000ms に設定します。
82 行目 時刻を取得する関数 (後述) get_tim() のみを繰り返し呼び出します。
ボタンおよび、パブリッシュ、スクライブはイベントが発生した時に処理されます。
80 var repeat_duration = 1000;
81   setInterval(function(){
82   get_time();
83 },repeat_duration);

ボタン bt_off をクリックした時の処理
87 行目 bt_off がクリックされた時に { } 内の処理を行います。
88 行目 JSON 形式のメッセージを変数 buf に格納します。
   ”date”    現在時刻を ISO8601 準拠形式で設定します。
   ”client”    任意の値。ここでは BROWSER とします。
   ”state”    IoT デバイスからの状態の表示に使用するのでここでは “NULL” とします。
   ”control”  ブラウザからの制御を設定するのでここでは “OFF_” とします。
89 行目 パブリッシュする関数 (後述) に変数 buf を引数として渡して実行します。
87 function bt_offClicked(elment){
88    const buf = "{\"date\":\""+date2+"\",\"client\":\"BROWSER\",\"state\":\"NULL\",\"control\":\"OFF_\"}" 
89   pub_msg(buf); 
90 }

ボタン bt_on は bt_off と同様なので省略します。

パブリッシュする関数
100 行目 変数 payload に 引数 msg_publish を代入します。
      (そのまま msg_publish を使っても良かったかもしれません。)
101 行目 引数 topic, Payload, qos、 戻り値 error を設定して client.publish( ) を実行します。
102-104 行目 エラーが発生した場合にはコンソールに出力します。
106 行目 payload の内容を画面に表示します。
 99 function pub_msg(msg_publish){
100   payload = msg_publish;
101   client.publish(topic, payload, { qos }, (error) => {
102     if (error) {
103      console.error(error)
104     }
105  })
106   document.getElementById("msg_pub").innerHTML = payload.toString();
107   return;
108 }

メッセージをサブスクライブする関数
112 行目 引数として topic, Payload, qos、 戻り値 error を設定して client.subscribe( ) 関数を実行します。
この関数が実行されるとコールバック関数 client.on(“message”, (topic, payload) => { }) が起動されてメッセージを受信することができます。 ( 63 行目)
113-116 行目 エラーが発生した場合にはコンソールに出力します。
117 行目 コンソールに topic の内容を表示します。
111 function sub_msg(){
112   client.subscribe(topic, { qos }, (error) => {
113    if (error) {
114         console.log("Subscribe error:", error)
115         return
116     }
117      console.log(`Subscribe to topic ${topic}`);
118   })
119   return;
120 }

124-131 行目 現在時刻を取得して ISO 8061 に準拠した形に整えて変数 date2 に格納します。
        date2 はパブリッシュする際にメッセージに組み込みます。
133 行目 date2 はブラウザ画面にも表示します。
123 function get_time(){
124   const date1 = new Date();
125   date2 = date1.getFullYear() + "-" + 
126   ("0"+(date1.getMonth() + 1)).slice(-2)  + "-" + 
127   ("0"+(date1.getDate())).slice(-2) + "T" +
128   ("0"+(date1.getHours())).slice(-2) + ":" + 
129   ("0"+(date1.getMinutes())).slice(-2) + ":" + 
130   ("0"+(date1.getSeconds())).slice(-2) + "." +
131   String(date1.getMilliseconds()).padStart(3, "0") ;
132   //ミリセカンド( 0 ~ 999 整数)は 0 で埋めて 3 桁に揃える
133   document.getElementById("msg_time").innerHTML = date2;
134   return;
135 }

CSS コード

図 5 に CSS コード全文を示します。右上隅にカーソルを合わせると一括コピーできます。


/* MQTT_VPS.css 
   MQTT over WebSocket with TLS
*/
@charset "utf-8";
@media screen and (max-width:1080px) {
	/* 画面サイズが1080px以下の場合ここの記述が適用される */
  main { width: 100%; }
}
body { font-family: "Arial", "メイリオ"; }
h1 { font-size:16px; font-weight: normal; text-align: center; }
h2 { font-size:16px; font-weight: normal; text-align: center; }
textarea { resize: none; }
/* # id 参照 */
#msg_time{ text-align: center; }
#msg_pub_ttl{
  font-size: 12px; display:flex; justify-content: center;
}
#msg_sub_ttl{
  font-size: 12px; display:flex; justify-content: center;
}
#msg_pub {
  display: inline-block;
  min-width:340px; min-height: 65px; line-height: 2;
  font-size: 12px; color:black;
  background-color:white; padding: 10px; border-radius: 5%; border: 1px solid #ccc;
  box-shadow: 1px 1px 1px #999;
}
#msg_sub {
  display:inline-flex;
  min-width:340px;  min-height: 65px; line-height: 2;
  font-size: 12px; color:black; 
  background-color:white; padding: 10px; border-radius: 5%; border: 1px solid #ccc;
  box-shadow: 1px 1px 1px #999;
}
/* . class 参照 */
.inline_1{ text-align: center; font-size: 32px; }
.inline_2{ text-align: center; font-size: 12px; }
.inline_3{ text-align: center; font-size: 12px; }
.inline_4{ text-align: center; font-size: 12px; }
.bt_on {
  margin-right:50px;
  display:inline-block; width:140px; height:64px; padding:10px 10px; vertical-align: middle;
  text-decoration:none; font-size:32px; font-weight: bold ;background-color:aliceblue; color: gray;
  border-color:gray; border-radius: 4px;
}
.bt_on:active {
  background-color: tomato;color:aliceblue;
  -webkit-transform: translateY(0px); transform: translateY(0px);
}
.bt_off {
  display:inline-block; width:140px; height:64px; padding:10px 10px; vertical-align: middle;
  text-decoration:none; font-size:32px; font-weight: bold; background-color:aliceblue; color:gray;
  border-color:gray; border-radius: 4px;
}
.bt_off:active {
  background-color: teal; color:aliceblue;
  -webkit-transform: translateY(0px); transform: translateY(0px); 
}

図 5 CSS コード

端末対応
6-9 行目 スマートフォンの画面にも対応させています。
6 @media screen and (max-width:1080px) {
7	/* 画面サイズが1080px以下の場合ここの記述が適用される */
8  main { width: 100%; }
9 }

その他の Web ブラウザ画面の表示を整えている CSS については省略します。

動作確認

CA 証明書と作成した HTML、 Java Script、CSS コードを 任意の Web 公開フォルダにアップロードします。

-rw-r--r-- 1 www-data www-data 1939 Nov  5 17:53 ISRG_Root_X1.pem
-rw-r--r-- 1 www-data www-data 2171 Nov  5 17:53 MQTT_VPS.css
-rw-r--r-- 1 www-data www-data 1167 Nov  5 17:53 MQTT_VPS.html
-rw-r--r-- 1 www-data www-data 4739 Nov  5 17:53 MQTT_VPS.js


Web ブラウザに https://ドメイン名/MQTT_VPS.html を入力すると次の画面が立ち上がります。
 ・ドメイン名・・・ファイルをアップロードした任意の Web 公開フォルダのドメイン名です。
画面を立ち上げた後は現在時刻が 1 秒毎に更新されます。( 図 6 )

図 6

ON ボタンをクリックするとパブリッシュしたメッセージ(payload の内容)が published 窓に表示されます。 同時にエコーバックによりサブスクライブしたメッセージが subsclibed 窓に表示されます。 表示されたメッセージには “control”:”ON__” が含まれていて ON ボタンを押したことがわかります。( 図 7 )

図 7

このときブラウザのコンソールには次の内容が表示されています。( Ctrl + Shift + i )

Received Message: {"date":"2025-11-15T10:52:34.806","client":"BROWSER","state":"NULL","control":"ON__"}
On topic: MQTT topic

OFF ボタンをクリックするとパブリッシュしたメッセージ(payload の内容)が published 窓に表示されます。 同時にエコーバックによりサブスクライブしたメッセージが subsclibed 窓に表示されます。 表示されたメッセージには “control”:”OFF_” が含まれていて OFF ボタンを押したことがわかります。( 図 8 )

図 8

このときブラウザのコンソールには次の内容が表示されています。

Received Message: {"date":"2025-11-15T11:05:29.806","client":"BROWSER","state":"NULL","control":"OFF_"}
On topic: MQTT topic

以上 HTML, Java Script, CSS でコードを作成して Web ブラウザと MQTT Broker 間で MQTT over WebSockets with TLS (wss://) 通信を行うことができました。

(YI)

コメント

error: Content is protected !!
タイトルとURLをコピーしました