NATを越えよう！MQTTを使って家の外からLチカ

## はじめに

趣味で宇宙開発を行う団体「[リーマンサット・プロジェクト](https://www.rymansat.com)」がお送りする[新春アドベントカレンダー](https://qiita.com/rsp-dgkz/items/a6ff573c07dbe2c6166f)です。
僕はリーマンサットで[超小型人工衛星である「RSP-00」と「RSP-01」](https://www.rymansat.com/satellites)の無線通信のソフトウェア開発を担当しています。

## この記事は

この記事では、IoTでは**なぜMQTTというプロトコルが使われるかの解説**と、簡単な**MQTTブローカーの構築**とmbedを使った**MQTTクライアントの実装**をしました。
MQTTが使えると**スマートホームのように遠隔でモノの制御ができたり**します。衛星との通信はTCP/IPではありませんが、将来的にMQTTといった上位レイヤーの通信ができても面白いななんて考えてます。

ちなみに、**今回作ったプログラムはインターネット上に設置してないので、NAT越えはしてませんｗ**
インターネット上に設置すれば家の外からでもLチカができますが、今回はセキュリティとかはすっ飛ばしているので利用には注意が必要です。
セキュリティが苦手であればマネージドMQTTサービスである[AWS IoT](https://aws.amazon.com/jp/iot/)などを使った方が安全かもです。

### 成果物

ブラウザからの操作で基板上のLチカと、デバイスのボタンを押してサーバへログが残るところまでを実装しました。
動きとしてはこんな感じとなります。（GIFアニメです。）

![Lチカ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14453/f6c7b320-892a-44a8-5374-76f574747a25.gif)
![App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14453/0804212b-21e9-3c72-452e-4274186c24f4.png)

## なぜMQTT？

通常、家庭や会社のネットワーク環境はインターネットに接続するためのルーターにNATやFirewallといった機能が有効になっていることから、**インターネットから家の中のネットワークへアクセスすることはできません。**
![Network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14453/d07b7139-9fde-972e-3488-fd78ed551985.png)

MQTTは**双方向の非同期通信プロトコル**です。事前にTCPソケットを準備しておくことで**NATやFirewallが有効な環境でもクライアントとサーバーがお互い好きなタイミングで通信をおこなう事ができる**ため、**IoTやスマートホーム等で利用されている**プロトコルになります。
※Firewallの設定によっては通信できない場合もあります。
![MQTT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14453/bd94ab2b-5357-bd91-15ea-6de2e0de1516.png)
※QoS0の時のシーケンスになります。（Publish後のACK処理が異なります。）
※今回はあえてクライアント間の通信については触れていません。
※Publish/Subscribe以外の機能についても触れていません。

### HTTP

ではMQTTではなく一般的にスマホやPCのブラウザからWEBサイトを閲覧する時に利用されるHTTPだとどうでしょうか。
HTTPは**クライアント（スマホ等）がリクエストを送信し、サーバがレスポンスを返却するまでを1セットとした同期通信のプロトコル**です。
通常のWEBサイトの閲覧には適していますが、インターネット上から家の中のデバイスへアクセスするとなった場合、TCPハンドシェイクをインターネット側から開始する必要があるため、NATやFirewallに阻まれてしまいます。
一方でMQTTとは異なり同期通信であることから**非常に扱いやすい**といったメリットもあります。
![HTTP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14453/596d097a-9523-b671-6ae7-7b4de49b7b54.png)
※Keep-Aliveには触れていません。(TCP Closeせずにソケットが再利用されます。)

## 実装

### 開発環境

#### ハードウェア

- Macbook Pro(Retina, 13-inch, Late 2013)
- NUCLEO-F767ZI

#### ソフトウェア

- macOS Big Sur v11.1
- Node.js v14
- Aedes(MQTTブローカーライブラリ) v0.14
- Mbed Studio
- Mbed OS v6.6

[去年の新春アドベントカレンダーでVSCode + Mbed Cliで環境構築しました](https://qiita.com/kohei0302/items/6aa927907fe868ef0b50)が早速Mbed Studioに浮気しましたｗ

### サーバ・ブラウザのプログラム

今回はNode.jsを使ってサーバ側の実装をしています。
MQTTブローカーにはメジャーだった[Mosca](https://github.com/moscajs/mosca)の後継である[Aedes](https://github.com/moscajs/aedes)というライブラリを使用してみました。

```shell
$ npm install aedes
```

同時にHTTPのサーバも実装することでブラウザからのアクセスも可能にしています。
面倒だったのでブラウザに認識させるHTMLとjsはサーバのプログラムに組み込んでしまっていますｗ


```javascript:index.js
const http = require('http');
const aedes = require('aedes')();
const net = require('net');

const HTTP_PORT = 3000;
const MQTT_PORT = 1883;

// HTTPサーバの開始
http.createServer((req, res) => {
  switch (req.url) {
    // 「/」へのアクセス時にHTMLとjsを返却する
    case '/':
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(`
        <button id="button">LED</button>
        <script>
          document.getElementById('button').addEventListener('click', async () => {
            await fetch('/api/publish/led', {
              method: 'POST',
              credentials: 'same-origin',
              headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
              },
            });
          });
        </script>
      `);
      break;

    // 「/api/publish/led」アクセス時にMQTTのPublishをする
    case '/api/publish/led':
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ result: 'OK' }));
      aedes.publish({ topic: 'led', payload: "hogehoge" });
      break;
    default:
      res.writeHead(404).end();
  }
}).listen(HTTP_PORT, () => {
  console.log('Start HTTP Server!');
});

// MQTTブローカーの開始
net.createServer(aedes.handle).listen(MQTT_PORT, () => {
  console.log('Start MQTT Server!');
});

// 「button」トピックへpublishされたときにログを出力
aedes.subscribe('button', (packet, cb) => {
  console.log('button pushed!');
  cb();
}, () => {
  //
});

// クライアントがCONNECTパケットを送ってきた時にログを出力
aedes.on('client', (client) => {
  console.log('Client Connected: \x1b[33m' + (client ? client.id : client) + '\x1b[0m');
});

// クライアントがSUBSCRIBEパケットを送ってきた時にログを出力
aedes.on('subscribe', (subscriptions, client) => {
  console.log('MQTT client \x1b[32m' + (client ? client.id : client) +
    '\x1b[0m subscribed to topics: ' + subscriptions.map(s => s.topic).join('\n'));
});
```

### デバイスのプログラム

[mbed](https://os.mbed.com/)はArm Cortex-Mのマイコンボードを対象とした組み込みOS（ライブラリ）および開発環境です。
マルチプラットフォームでの開発が可能かつ、豊富なAPIやドキュメントが充実していることから僕は好んで使っています。

mbedは標準でEthernetの制御やTCP/IPの通信をする事ができますが、MQTTについてはmbedのライブラリとして公式から別途提供されているので、そちらを利用してブローカーへ接続しています。

```shell
$ mbed add https://github.com/ARMmbed/mbed-mqtt.git
```

```c++:main.cpp
#include "EthernetInterface.h"
#include "InterruptIn.h"
#include "MQTTClientMbedOs.h"
#include "mbed.h"

#define MQTT_HOST <MQTTブローカーのホスト名>
#define MQTT_PORT 1883
#define MQTT_CLIENT_ID "NUCLEO_F767ZI"

InterruptIn button(USER_BUTTON, PullDown);
DigitalOut led(LED1, 0);
Timeout timeout;

EthernetInterface net;
TCPSocket socket;
SocketAddress myAddr;
SocketAddress serverAddr(MQTT_HOST, MQTT_PORT);
MQTTClient client(&socket);

bool buttonFlag = false;

void onReceiveTopicLed(MQTT::MessageData &md);

int main() {
  printf("MQTT Client ID: %s\r\n", MQTT_CLIENT_ID);

  // Ethernetの初期化、DHCPでIPの取得
  printf("Ethernet Interface Initializing...\r\n");
  net.connect();
  net.get_ip_address(&myAddr);
  printf("Ethernet Interface Initialized\r\n");
  printf("IP address: %s\n",
         myAddr.get_ip_address() ? myAddr.get_ip_address() : "None");

  // MQTTサーバへ接続
  printf("MQTT Server Connecting...\r\n");
  socket.open(&net);
  socket.connect(serverAddr);
  MQTTPacket_connectData connectData = MQTTPacket_connectData_initializer;
  char clientID[] = MQTT_CLIENT_ID;
  connectData.clientID.cstring = clientID;
  client.connect(connectData);
  printf("MQTT Server Connected\r\n");

  // 「led」トピックのサブスクライブ
  client.subscribe("led", MQTT::QOS0, &onReceiveTopicLed);
  printf("MQTT Subscribe topic: led\r\n");

  // ボタンが押されたらフラグを立てる
  button.rise([] { buttonFlag = true; });

  while (true) {
    client.yield(1);

    // ボタンフラグが立っていれば「button」トピックをpublishする
    if (buttonFlag) {
      char payload[] = "fugafuga";
      MQTT::Message message;
      message.qos = MQTT::QOS0;
      message.retained = false;
      message.dup = false;
      message.payload = payload;
      message.payloadlen = strlen(payload);
      client.publish("button", message);
      printf("MQTT Publish topic: button\r\n");
      buttonFlag = false;
    }
  }
}

// 「led」トピックを受信した時のコールバック
void onReceiveTopicLed(MQTT::MessageData &md) {
  char payload[128] = {0};
  char topicName[128] = {0};
  memcpy(payload, md.message.payload,
         md.message.payloadlen < 100 ? md.message.payloadlen : 100);
  memcpy(topicName, md.topicName.lenstring.data,
         md.topicName.lenstring.len ? md.topicName.lenstring.len : 100);

  // LEDを1秒間ONにする
  led = 1;
  timeout.detach();
  timeout.attach([] { led = 0; }, 1s);

  printf("MQTT receive message %s on %s\r\n", payload, topicName);
}
```

## おわりに

リーマンサット・プロジェクトは「普通の人が集まって宇宙開発しよう」を合言葉に活動をしている民間団体です。
他では経験できない「宇宙開発プロジェクト」に誰もが携わることができます。
興味を持たれた方は [https://www.rymansat.com/join](https://www.rymansat.com/join) からお気軽にどうぞ。

次回は @KaitosLab さんの「Webエンジニアになる！こと初め」です。楽しみですね！