31
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-19

はじめに

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

この記事は

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

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

成果物

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

Lチカ.gif
App.png

なぜMQTT?

通常、家庭や会社のネットワーク環境はインターネットに接続するためのルーターにNATやFirewallといった機能が有効になっていることから、インターネットから家の中のネットワークへアクセスすることはできません。
Network.png

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

HTTP

ではMQTTではなく一般的にスマホやPCのブラウザからWEBサイトを閲覧する時に利用されるHTTPだとどうでしょうか。
HTTPはクライアント(スマホ等)がリクエストを送信し、サーバがレスポンスを返却するまでを1セットとした同期通信のプロトコルです。
通常のWEBサイトの閲覧には適していますが、インターネット上から家の中のデバイスへアクセスするとなった場合、TCPハンドシェイクをインターネット側から開始する必要があるため、NATやFirewallに阻まれてしまいます。
一方でMQTTとは異なり同期通信であることから非常に扱いやすいといったメリットもあります。
HTTP.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で環境構築しましたが早速Mbed Studioに浮気しましたw

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

今回はNode.jsを使ってサーバ側の実装をしています。
MQTTブローカーにはメジャーだったMoscaの後継であるAedesというライブラリを使用してみました。

$ npm install aedes

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

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

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

$ mbed add https://github.com/ARMmbed/mbed-mqtt.git
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 からお気軽にどうぞ。

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

31
31
0

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
31
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?