micro:bitで気象情報・天気予報のMQTT通信

0. アイディア

気象情報・天気予報の作例

これらをみて、なんとか上りと下り両方でIoT(らしきもの)をやってみたい、と思い立ちました。意図した通りに動くものはできましたが、電力消費が激しく、モバイルバッテリーで常時使用は難しい状況であり、こういうこともできる、ということに過ぎません。

micro:bitの限界について

そしてやはり、慣れ親しんだmicro:bitを使いたいと考えましたがそこでネックになるのは、ネットワークへの接続です。micro:bitは単体でWifi接続はできないという限界があり、外部モジュールもWifiに接続できるものは(国内での正規利用を考えると)限られています1。単体でもBluetoothやシリアル通信は可能であるため、micro:bitを使う場合、上記作例も含め、Bluetoothやシリアル通信でラズパイ(Raspberry Pi)、M5シリーズ又はPCに接続、ラズパイ等をゲートウェイにして、インターネットに繋ぐ作例が多いようです。

(今回もそうですが)ゲートウェイ側のラズパイ等のプログラムも必要となってしまうので、そうすると最初からラズパイ等だけでやる(micro:bit不要で、ラズパイ等に直接センサを繋いだり表示したりする)、というのが本来は素直です。。

やりとりするデータについて

やりとりするデータのうち、上り通信の方の気象情報については、micro:bitは、 以前ご紹介したとおり、そのままでもセンサーが充実しており、温度や明るさであれば単体でも計測できます。実際、これまでも、温度と明るさを計測するプログラムを、MakeCode EditorのブロックプログラミングMicroPythonで作成してみたことがあります。他方、これ以外の気象情報、例えば、湿度や気圧の計測となると、外部センサーに頼る必要があります。

なお、下り通信の方の天気予報について、これは、micro:bitだからどうこうという話ではありませんが、今は気象庁の天気予報JSONが手軽でしょうか(気象庁の天気予報JSONファイルをWebAPI的に利用したサンプルアプリ | サンプルアプリ一覧 | あんこエデュケーション)。これを使って、改造Scratchでネコに天気予報をしゃべってもらうプログラムを作ったことはありました。もっとも、今回は楽をして別の方法をとっています。

今回の仕組み

今回の具体的な仕組みは、次の図のようになります。青色矢印が上り通信、オレンジ色矢印が下り通信です。

  • ネックとなるmicro:bitからのネットワークへの接続ですが、

  • データについては、

    • 上り通信のデータとして、気象情報は、BME280という外部センサで、気温・湿度・気圧を測ることにしました。
    • 下り通信の、天気予報については、本来は気象庁のウェブサイトから取得したりしたいのですが、簡略化して、スマートフォンiOS)で天気予報アプリから得た情報を、MQTT接続アプリを使って送信する、というショートカットを作成、それをオートメーション機能で定期実行しています。
  • micro:bitでの表示については、

    • 気象情報は、Aボタンを押すと、外部のOLEDに表示されるようにし、
    • 天気予報は、Bボタンを押すと、micro:bit本体のLEDで降水確率に応じたアイコン表示する形です。

1. 使用したもの

ハード

ソフト

  • 「ショートカット」アプリ(iOS標準)
  • 天気アプリ(iOS標準)
  • MQTT Analyzer(iOSのMQTTクライアント、「ショートカット」からも利用可能)

→「ショートカット」で天気予報アプリの取得情報(降水確率)をMQTT Analyzerでpublishする設定は次のとおり

  • MQTT dashboardAndroidのMQTTクライアント、こちらはJSONの読み込みに加えて、受信した数値のグラフ表示も可能であり、使い勝手がよいです。)

2. 上り通信の雰囲気(気象情報のPublish)

配線は特殊な点はありませんが、外部センサ・外部ディスプレイと、micro:bitを、(M5:Bit経由で)I2C接続する際、繋ぎ間違えないようにするくらいでしょうか。M5:Bitは、エッジコネクタとの接続部を上にしたとき右側に2セットのI2C接続端子があり、DA/CL/V/Gの表示もされているので分かりやすいです。

3. 下り通信の雰囲気(天気予報のSubscribe)

4. コード

micro:bit

  • 初期設定(「最初だけ」)

    • シリアル通信は、TXをP8、RXをP12としています。これは、M5:BitのGroveコネクタ経由での接続の割り当てがデフォルトでこのようになっているためです。
    • LCDディスプレイはこちら、センサ(BME280)はこちら拡張機能を使っており、I2Cのアドレスについて、LCDは60、センサは76を指定しています。
  • ループ(「ずっと」)

    • 1時間ごとに、センサでの気象情報の読み取り(変数への格納)と、読み取った気象情報のM5Cameraへのシリアル通信(上り通信)を行っています。この気象情報は、MQTTブローカーを経由してスマホでsubscribeします。
    • MQTTのクライアント(スマホ)での表示を考慮して、JSON形式にしています。JSON形式にするための拡張機能は見当たらなかったのですがmakecodeのdocsのData Analysis > Remote Data の最後の箇所を参考にして書いています。
  • シリアル通信で受信した時(下り通信)

    • スマホ側でpublishした天気予報の降水確率を、MQTTでsubscribeしているM5Camera経由で受信した時に、変数に格納します。
  • ボタン操作

    • Aボタンが押されると、センサで読み取った気象情報をOLEDディスプレイで表示します。

    • Bボタンが押されると、下り通信で取得した天気予報の降水確率に応じて、晴れ・雨・曇りマークをmicro:bit本体のLEDに表示しています。

MakeCodeの画面でJavaScriptに切り替えたコードは以下のとおりです。

input.onButtonPressed(Button.A, function () {
    OLED12864_I2C.showString(
    0,
    0,
    "Temp: " + convertToText(temperature),
    1
    )
    OLED12864_I2C.showString(
    0,
    1,
    "Humi: " + convertToText(humidity),
    1
    )
    OLED12864_I2C.showString(
    0,
    2,
    "Pres: " + convertToText(pressure),
    1
    )
    basic.pause(5000)
    OLED12864_I2C.clear()
})
serial.onDataReceived(serial.delimiters(Delimiters.NewLine), function () {
    weather = parseFloat(serial.readUntil(serial.delimiters(Delimiters.NewLine)))
})
input.onButtonPressed(Button.B, function () {
    if (weather < 0) {
        basic.showIcon(IconNames.Asleep)
    } else if (weather < 40) {
        basic.showLeds(`
            # . # . #
            . # # # .
            # # # # #
            . # # # .
            # . # . #
            `)
    } else if (weather > 60) {
        basic.showIcon(IconNames.Umbrella)
    } else {
        basic.showLeds(`
            . . . . .
            . # # # .
            # # # # #
            . # # # .
            . . . . .
            `)
    }
    basic.pause(5000)
    basic.clearScreen()
})
let pressure = 0
let humidity = 0
let temperature = 0
let weather = 0
basic.showIcon(IconNames.Giraffe)
basic.pause(500)
basic.clearScreen()
weather = -1
temperature = 0
humidity = 0
pressure = 0
serial.redirect(
SerialPin.P8,
SerialPin.P12,
BaudRate.BaudRate115200
)
OLED12864_I2C.init(60)
BME280.Address(BME280_I2C_ADDRESS.ADDR_0x76)
basic.pause(10000)
basic.forever(function () {
    temperature = BME280.temperature(BME280_T.T_C)
    humidity = BME280.humidity()
    pressure = BME280.pressure(BME280_P.hPa)
    serial.writeString("{")
    serial.writeString("\"temperature\":" + temperature + ",")
    serial.writeString("\"humidity\":" + humidity + ",")
    serial.writeString("\"pressure\":" + pressure)
    serial.writeLine("}")
    basic.pause(3600000)
})

M5Camera側

M5Camera側は、Arduino IDE上で、以下のコードを作成して書き込んでいます。

  • MQTTクライアントは、Arduino Client for MQTTを使用しています。
  • WifiSSIDとパスワードやMQTTのIPアドレスやポート番号の設定はそれぞれの環境にあったものを入力する必要があります。MQTTのユーザやトピックは任意ですが、トピックは同じものをスマホのMQTTクライアントで設定することになります。

  • micro:bitとのシリアル通信(前述の通り、GroveコネクタとM5:Bit経由)は、Serial2.begin()の箇所で初期設定しており、ボーレートはmicro:bitの設定と合わせ、TXは4、RXは13としています(M5Cameraの場合。他のM5シリーズを利用する場合などは異なります)。Serial2.begin()のパラメーターについてはこちらを参考にしました。

  • その他のコードの説明は、簡単なものですがコメントを入れています。定期処理はこのままですとかなり電力を消費するため、間隔を開けて実行するなど省電力化の工夫が必要となりそうです。

#include <WiFi.h>
#include <PubSubClient.h>

// 各種設定値
const char* ssid = "[SSIDを入力]";
const char* password = "[パスワードを入力]";
const char* mqtthost = "[MQTTブローカーのIPアドレス]";
const int mqttport = "[MQTTブローカーのポート番号]";
WiFiClient  wfClient;
PubSubClient client(wfClient);

const char* userid = "[ユーザIDを入力]";
const char* topic_pub = "[Subscribeするトピック(天気予報)を入力]";
const char* topic_sub = "[Publishするトピック(気象情報)を入力]";
String message;
const int len = 50;
char payload_pub[len];

// Subscribe用の処理(天気予報のトピックをSubscribeしておきスマホのMQTTクライアントからMQTTブローカー経由で受診した場合、micro:bitにシリアル通知で送信する。)
void callback(char* topic, byte* payload, unsigned int length) {
  for (int i=0; i<length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
  for (int i=0; i<length; i++) {
    Serial2.print((char)payload[i]);
  }
  Serial2.println();
}

void setup() {
  Serial.begin(9600);
  Serial.println();

  //M5CameraのLED
  pinMode(14, OUTPUT);
  
  setup_wifi();
  setup_mqtt();

  //micro:bitとのシリアル通信用の初期設定(M5Cameraの場合、TXは4、RXは13とする。)
  Serial2.begin(115200, SERIAL_8N1, 13, 4);

  //M5CameraのLEDを点滅
  digitalWrite(14, HIGH);
  delay(100);
  digitalWrite(14, LOW);
  delay(100);
}

// Wifi接続
void setup_wifi() {
  Serial.println();
  Serial.print("connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("WiFi connected at ");
  Serial.print(WiFi.localIP());
}

// MQTT接続
void setup_mqtt(){
  Serial.println();
  Serial.print("connecting to mqtthost");
  client.setServer(mqtthost, mqttport);
  //subscribe
  client.setCallback(callback);
  while (!client.connected()) {
    delay(1000);
    Serial.print(".");
    client.connect(userid);
  }
  Serial.println();
  Serial.print("mqtt connected as ");
  Serial.println(userid);
  // Subscribe用の処理
  client.subscribe(topic_sub);
  Serial.print("mqtt subscribed for ");
  Serial.println(topic_sub);
}

// 定期処理
void loop() {
  // Publish用の処理(micro:bitからのシリアル通信を読み取る。読み取ったデータはJSON形式であるが、特にここでは要素の処理はせず、const char[ ]に変換して、Publishするのみ。)
  if (Serial2.available() > 0) {
    message = Serial2.readStringUntil(0x0a);
    message.toCharArray(payload_pub, len);
    client.publish(topic_pub, payload_pub, true);
    // client.publish(topic_pub, (uint8_t*)payload_pub, (uint8_t)len, true);
    Serial.print("published ");
    Serial.print(topic_pub);
    Serial.print(" as ");
    Serial.println(payload_pub);
    delay(500);
  }
  // Subscribe用の処理
  client.loop();
}

  1. 例えば、grove shield for microbit v2を経由したgrove uart wifi v2というモジュールの利用が考えられます。これは既存の拡張機能によりIFTTT 経由でLINEに通知でき、また、ATコマンドを利用してウェブサイトへのアクセスも可能です(サヌキテックネットさんのGroveデバイスに関する一連の記事の15-3〜15-6)。そのため、実は、このモジュールだけで、MQTTブローカーに接続できるかもしれません。MQTTの構造と、ATコマンドをしっかり理解する必要がありそうですが、MQTTで始めるIoTデバイスの作り方 - MONOist参照。

  2. なお、上記で参照した作例中、「IoTをはじめよう 室温管理システムを作る」は、M5シリーズのうちM5Stackとmicro:bitを接続しています。ここでは、M5:Bitは使わずに、通信はGroveケーブル経由でジャンパワイヤ・エッジコネクタ経由で繋ぎ(なお、この方法が実際可能かは、現行機種の使用も含めてご確認ください)、電源もM5Stackの3.3V端子を使っています。M5Cameraでは同じ方法は試していませんが、少なくとも電源については、M5Cameraは3.3V端子がないため、同じ方法ではできないことになりそうです。