Qtブログ(日本語)

Qt の MQTT でデバイス間通信の最適化

作成者: 鈴木 佑|Sep 18, 2018 5:52:19 PM

この記事は The Qt BlogOptimizing Device Communication with Qt MQTT を翻訳したものです。
執筆: Maurice Kalinowski, 2018年05月16日

Qt for Automation は Qt for Application Development や Qt for Device Creation 向けのアドオンとして、Qt 5.10 に合わせてリリース致しました。これに含まれる Qt MQTT と呼ばれるモジュールが、クライアントサイドの MQTT のソリューションです。

Qt MQTT はクライアント用で、開発者が MQTT で送るデータを管理するセンサーデバイスやゲートウエイを開発する際に利用されます。MQTT はそれ自身が軽量で、オープンで、シンプルだとうたっています。プロトコル的にオーバーヘッドを限りなく減らすというのが基本的な思想です。もっとも採用されているバージョンは MQTT 3.1.1 で、これはよく MQTT 4 と呼ばれています。MQTT 5 は最近標準化がなされ、Qt MQTT でもその対応を進めています。これについては今年の後半に別途お知らせ致します。

このモジュールの機能の確認と、開発者への利用のガイドラインを作成するために、The Qt Company は SensorTag と呼ばれるデモを開発しました。このブログ記事のシリーズではこのデモが何度も登場します。

簡単な紹介動画をまずはご覧ください。

[embed]https://www.youtube.com/watch?v=k-wrIixUcKQ[/embed]

このデモのソースコードは、Boot 2 Qt のデモ用のリポジトリ( http://code.qt.io/cgit/qt-apps/boot2qt-demos.git/tree/tradeshow/iot-sensortag )にあります。

基本的に、複数のセンサーがデータを Bluetooth を利用してゲートウェイ(今回は raspberry Pi 3)に送り、その後クラウド上の MQTT ブローカーに情報を送るといったシナリオでこのデモは動作しています。複数のゲートウェイが組み合わさってセンサーネットワークを形成しています。

それぞれのセンサーはその計測データを送りつづけます。具体的には、

  • 周囲の温度
  • センサーの温度
  • 加速度
  • 角度、角速度
  • 磁気
  • 高度
  • 照度
  • 湿度

この連載の目的はデータの流れのため、このセンサーとゲートウェイの組み合わせを「デバイス」と呼ぶことにします。

このデモには、見た目でアピールできるようなユーザーインターフェースであることと、MQTT(と Qt MQTT)が簡単に使えることの2つの目標があります。

MQTT は Publish/Subscribe プロトコルのため、データは特定のトピックに紐づいて送信され、他者は自分自身をブローカーに登録することで発行されたメッセージの通知を受けとることが可能です。

上記のデバイスで、利用可能なメッセージは以下のとおりです。

  • [トピック:Sendors/active、データ:ID]:5秒ごとにセンサーは「オンライン」のメッセージを発行し、サブスクライバにそのデバイスはアクティブでデータの送信を行うことを通知します。接続開始時には「Offline」という Will メッセージもそこに含めます。この Will メッセージは、そのデバイスの接続が切れた際にすべてのサブスクライバに通知する際に付与される「Last Will」で使われます。そのため、ネットワークの接続が切れた瞬間に、ブローカーはその Will をすべてのサブスクライバにブロードキャストします。
  • [トピック:Sensor///、データ:]:これは各センサーが送るもので、datatype のデータの値が変わった際に常に送ります。動画のユーザーインターフェースはこのメッセージを監視し、表示をそれに合わせて変更しています。

余談:追加のユースケースとしては、すべてのデバイスの温度のチェックがあげられます。この場合には「ワイルドカード」サブスクリプションが便利です。”Sensor/+/temparature” とすることで、すべてのセンサーからの温度の更新を受けとることが可能になります。

今のところセンサー数が一番多いのは展示会でのデモの時で、10個程度のセンサーが同時に動いています。前述のとおり、このデモは使いやすさを示すもので、パフォーマンスや電力消費をそれほど考慮していません。製品化に際しては以下のような問題を解決する必要があります。

  • 実世界でのシナリオでは何が起こりうるか?このデモは数千ものデバイスがデータを送る場合にスケールするか?
  • ハードウェアの仕様やバッテリー消費の条件を満たせるか?
  • Low Power WAN の通信の制限範囲内で実現が可能か?

LoRA や Sigfox といった LPWAN については、Qt World Summit 2017 での Massimo の講演(https://www.youtube.com/watch?v=s9h7osaSWeU) をご覧ください。

 

ソースコードを単純化して、UI のない簡単なものにした最低限のサンプルコードを こちら に置きました。この中で、SensorInformation クラスは頻繁に更新されるいくつかのプロパティを持ちます。

class SensorInformation : public QObject

{
Q_OBJECT
Q_PROPERTY(double ambientTemperature READ ambientTemperature WRITE setAmbientTemperature NOTIFY ambientTemperatureChanged)
Q_PROPERTY(double objectTemperature READ objectTemperature WRITE setObjectTemperature NOTIFY objectTemperatureChanged)
Q_PROPERTY(double accelerometerX READ accelerometerX WRITE setAccelerometerX NOTIFY accelerometerXChanged)
Q_PROPERTY(double accelerometerY READ accelerometerY WRITE setAccelerometerY NOTIFY accelerometerYChanged)
Q_PROPERTY(double accelerometerZ READ accelerometerZ WRITE setAccelerometerZ NOTIFY accelerometerZChanged)
Q_PROPERTY(double altitude READ altitude WRITE setAltitude NOTIFY altitudeChanged)
Q_PROPERTY(double light READ light WRITE setLight NOTIFY lightChanged)
Q_PROPERTY(double humidity READ humidity WRITE setHumidity NOTIFY humidityChanged)
[..]

 

プロパティの値が変わる度に、クライアントはメッセージを発行します。

    m_client->publish(QString::fromLatin1("qtdemosensors/%1/ambientTemperature").arg(m_id),
                      QByteArray::number(ambientTemperature));

 

デバイスの更新頻度はセンサーによりまちまちで、たとえば温度や照度などは加速度に比べると緩やかです。

どのくらいのデータが送られるかについては、Part1B のサンプルで転送能力を確認しています。MQTT の標準化では転送に関しての要求はとても少なくなっています。送られるデータは、順番を守り、欠損なしに、双方向で行われる必要があります。理論的には、QIODevice の派生クラスはこれを満たします。QMqttClient では QMqttClient::setTransport() を利用して転送のカスタマイズが可能です。というわけで、以下のコードを見てみましょう。

 

class LoggingTransport : public QTcpSocket
{
public:
    LoggingTransport(QObject *parent = nullptr);

protected:
qint64 writeData(const char *data, qint64 len) override;
private:
    void printStatistics();
    QTimer *m_timer;
    QMutex *m_mutex;
    int m_dataSize{0};
};

コンストラクタで、printStatistics() を定期的に実行するタイマーを生成しています。writeData では送られるデータの長さを保持し、QTcpSocket::writeData() にデータを渡しているだけです。

LoggingTransport をクライアントに追加するには、以下のようにする必要があります。

    m_transport = new LoggingTransport(this);
    m_client = new QMqttClient(this);
    m_client->setTransport(m_transport, QMqttClient::AbstractSocket);

これにより、11秒後に1つのセンサーが100KBのデータを送っていることが分かります。これはセンサー1つあたりの量です。現実を考えた場合、これは明らかに受け入れられない量になるでしょう。このため、情報量を落とさずに、バイト数を削減することが必要になります。

このデモでは、一つの接続を常に使っていたため、切断や再接続は必要ありませんでした。MQTT 3.1.1 の接続に関するデータは約 10バイトにクライアントの ID を加えたものになります。データを送る度に再接続が発生すると、接続の部分だけでも大量のデータになってしまいます。そういう意味では、私たちはすでに「最適化を行った」と言えるでしょう。

次に、データの発送のステップに移ります。各メッセージのサイズを削減するのと、メッセージの量を減らすことを考えます。

前者では、周辺温度のメッセージは以下のように設計されています。

Bytes Content
1 Publish Statement & Configuration
1 Remaining Length of the message
2 Length of the topic
71 Topic: qtdemosensors/{8f8fde60-933d-44cf-b3a7-8dac62425a63}/ambientTemperature
2 ID of the message
1..8 Value (String for double)

トピック自体がメッセージのサイズを担当しているのは、主にペイロードが保持する値がメッセージのサイズの一部なためです。

ここで考えうる最適化は以下のとおり。

  • ルートトピックを短縮する(qtdemosensors -> qtds)
  • ID を短縮する(UUID -> 8桁の数字)
  • センサーのタイプを文字ではなく enum にする(ambientTemparature -> 1)

これらのアプローチには代償が伴います。文字列を enum にするとメッセージの可読性が下がり、サブスクライバは常にどの数値がどのタイプに該当するのかを知っている必要があります。ID のサイズの削減は、接続するデバイスの総数などに制限を設ける可能性があります。しかし、これらを採用することで 100KB のバリアに達するまでに3倍もの時間を稼げるようになります(Part 1C のサンプルを参照してください)。

この段階では、これ以上、情報を減らさずにメッセージのオーバーヘッドを削減することは不可能です。今回の例では、シミュレートされたセンサーが現場で利用されることに言及しましたが、LPWAN 経由でデータを送るというケースも考えられます。一般的に、この分野のセンサーはそういった高周波でデータを送るべきではありません。それでもそうする場合には、以下の2つを検討すべきでしょう。

はじめに、複数のメッセージを、すべてのセンサーの値を含む1つのメッセージとして結合することです。上記の表で、値の部分のデータ量はメッセージのオーバーヘッドの極一部だということを示しました。

1つの案は JSON でプロパティの値を表現することで、これは Part 1d のサンプルで行っています。QJsonObject と QJsonDocument はとても便利で、QJsonDocument::toJson() により内容を QByteArray 形式に変換することで MQTT のメッセージに乗せる事が可能です。

 

void SensorInformation::publish()
{
    QJsonObject jobject;
    jobject["AmbientTemperature"] = QString::number(m_ambientTemperature);
    jobject["ObjectTemperature"] = QString::number(m_objectTemperature);
    jobject["AccelerometerX"] = QString::number(m_accelerometerX);
    jobject["AccelerometerY"] = QString::number(m_accelerometerY);
    jobject["AccelerometerZ"] = QString::number(m_accelerometerZ);
    jobject["Altitude"] = QString::number(m_altitude);
    jobject["Light"] = QString::number(m_light);
    jobject["Humidity"] = QString::number(m_humidity);
    QJsonDocument doc( jobject );
    m_client->publish(QString::fromLatin1("qtds/%1").arg(m_id), doc.toJson());
}

 

Tすべての情報を含む MQTT のメッセージのサイズはおよそ 272 バイトになりました。前述のとおり、情報量の欠損を伴いますが、必要なバンド帯域は大幅に削減されました。

この最初のパートのまとめとしては、デバイスから MQTT ブローカーに送るデータ量を比較的簡単に、大幅に削減する方法をいくつか試してみました。可読性や拡張性を犠牲にしたものや、データ転送前に情報の欠損を伴うものもありました。実際にどうするかはユースケースや IoT のソリューションを製品化する際のシナリオ次第になります。しかし、今回紹介したものは Qt であれば簡単に実装が可能です。というわけで、今回は JSON, MQTT, ネットワークと Bluetooth 接続に関する内容をカバーしました。

ウェブサイトやこのポストのコメントを通してさらなる情報を得ながら、次回までお待ちください。