• 投稿日
  • M5stack で作る ON AIR サインで手に入れる平穏な在宅勤務環境

    みなさまこんにちは。
    ソラコムのエンジニア ogu です。

    近頃では新型コロナウイルスの感染拡大防止のため、自宅でお仕事をされるようになったという方も大勢いらっしゃることと思います。

    私どもソラコムでは元々リモートワーク前提な働き方をしているため、極端な話世界中のどこにいても PC とインターネットと電源さえあれば大半の仕事ができてしまうという状態で、私個人としては元々月に2〜3日ほどしかオフィスに出社しておらずそれ以外の日は自宅で仕事をしており、実は仕事環境としてはそれほど大きな変化は感じていないのですが、家に子どもたちが居る時間が長くなったのが変化といえば変化かもしれません。

    小さな子どもたちは親の都合には全くお構いなく仕事をしている部屋に乱入してきて話しかけてきたり、Zoom などで行っているビデオ会議に映り込んだり声が入ってしまったりして参加者の間に微笑ましい空気が流れる・・・なんてこともたまにありますね。
    社内会議だと微笑ましい空気で済むかもしれませんが、お客様との重要な打ち合わせの際にそんなことになったらと思うとドキドキしてしまいます。

    子供に限らず、家族の誰かが掃除機を掛け始めるとか、宅配の荷物が届いて「ピンポーン」と鳴ったりして、それらの音をマイクが拾ってしまう、なんてこともあるかもしれません。

    ピンポンはあまり防ぎようがないですが、家族には事前に会議の予定時刻などを伝えておくことで多少はリスクを軽減することができます。

    しかしうっかり予定を伝え忘れてしまったり、緊急の会議が入ったり、予定を伝えても家族が忘れてしまったり、小さな子はそもそもそんなことお構いなしに部屋に入ってきたりしてしまいます。

    今日は、そんなお悩みを解決するかもしれないソリューションをご紹介します。

    しかも、そのソリューション、M5Stack を使って、簡単に自分で作れちゃうんです。

    では早速いってみましょう。

    構想

    家族に事前に予定を伝え忘れたりしても、なるべく簡単に、すばやく「今は会議中である」ということが仕事部屋の外に掲出でき、それを見たら子供でもはっきりと「今は入ってはいけない」とわかるようにできたら良さそうですね。

    テレビやラジオの放送局によくある(というイメージなのですが実際にあるかどうかは不明)、オンエアーランプのようなものを作ることになりそうです。

    会議中は ON AIR と表示し、会議中でなければ何も表示しないというシンプルな仕組みを実現すればよいでしょう。

    この ON/OFF を自動的に実現できれば完璧です。

    では会議中であるかどうかはどうやって判断するでしょうか?

    人力でボタンを押して「今から会議」「会議終了」などを入力する?私は忘れっぽいので押すのを忘れてしまいそうです😅

    Google カレンダーの予定から拾って来ようかなとも思ったのですが、緊急の会議等ではカレンダーに入っていないこともありますし、予定されていた会議が無くなったりしてもカレンダー上は存在することになっていたりと、実際には精度がそれほど高くない懸念があります。

    ここでひらめきました。
    ビデオ会議をしているということは、パソコンのマイクとウェブカメラは使用中なはずなので、それらのデバイスが使用中かどうかを検出することができればかなりの精度で会議中かどうかを判別することができそうです。

    では、マイクやカメラが使用中かどうか、どうやって検出すればよいでしょう?

    私は仕事に使用しているパソコンは今は Linux (Ubuntu) がメインです。エンジニアの皆様ならおわかりかと思いますが Linux (UNIX) の世界ではすべてがファイルです。ということはマイクやカメラを表すファイルが存在するはずで、そのファイルを開いているプロセスがあるかどうかを調べればよいわけです。 (Windows や Mac をお使いの皆様はどうにか頑張って検出してください。)

    デバイスファイルを開いているプロセスを調べるには lsof というコマンドを使えば簡単にできます。

    マイクは /dev/snd/ ディレクトリ以下のデバイスファイル、カメラは /dev/video0 といった名前のファイルです。

    私の環境では /dev/snd/ ディレクトリ以下には10個以上ものデバイスファイルが存在していて、どれが電話会議のときに使われるファイルなのかを突き止めるのが面倒そうでした。一方でカメラのデバイスファイルは /dev/video0/dev/video1 しかなく、どちらもカメラの使用状況と連動していそうだったので今回はマイクは諦めてカメラの使用状況のみを検出することにしました。

    カメラを止めてしまうと実際には会議中であっても会議中でないと判定してしまうリスクは残りますが、カメラを止めることはあまりないのでそこは割り切ることにしました。

    デバイスファイルがわかってしまえば lsof コマンドの使い方は簡単です。

    $ lsof /dev/video0
    

    このようなコマンドを実行し、何らかのプロセスがカメラを使用していれば何らかの表示が行われ、どのプロセスもカメラを利用していなければ何も出力されません。

    そこで

    $ lsof /dev/video0 | wc -l
    

    として出力の行数を数えて 0 ならカメラ未使用、1 以上ならカメラ使用中と判断することができます。

    会議中かどうかの判断はこれだけで自動的にできそうです。思ったより簡単でした。

    では続いて ON AIR と表示するデバイスの検討です。

    スマホやタブレットのように画面のあるデバイスであれば、真っ赤な背景に白い文字で ON AIR と表示するのは難しくなさそうですし、バッテリーで動作してくれれば丸1日くらい部屋の外にぶら下げておいたりすることができそうです。

    私の手元には M5Stack Basic (以下、M5Stack)がありましたので、今回はこれを使うことにしたいと思います。
    (M5Stack にスタックできるバッテリー用の基板も販売されていますが、今回は試作なのでモバイルバッテリーで動作させようと思います)

    あとは、パソコン上で検出したカメラの使用状況を M5Stack に伝える方法を考えなければなりません。

    M5Stack は Wi-Fi を搭載しているので、パソコンと M5Stack が LAN 内で直接やり取りをしても良さそうですが、パソコンから M5Stack にデータをプッシュするには M5Stack 側に何らかのサーバーを立てなければならず、M5Stack からパソコン側にアクセスしてデータをプルするにしてもパソコン側に何らかのサーバーが必要です。通信プロトコル(REST API にするとしたら API の Path や Method、レスポンスを JSON にするかどうかなど)を決めたりする必要もあり、意外と簡単なようで難しいという印象です。

    そこで私は MQTT を使うことを考えました。MQTT ブローカーを介して、パソコンは MQTT ブローカーにデータを Publish するだけ、M5Stack は MQTT ブローカーに Subscribe してデータを受信する、というアーキテクチャです。

    MQTT ブローカーを PC の内部に立てたりしてもいいのですが、今回は無料で使えるパブリックなクラウドサービスを利用することにしました。

    パブリックなサービスを利用することで、自分で構築したりメンテナンスしたりする手間が減るだけでなく、可視化のサービスが最初からついていたりしてデバッグなどに便利というメリットもあります。

    いくつかそのようなサービスが公開されていますが、今回は Adafruit の提供している Adafruit IO を利用することにしました。
    Adafruit IO を利用することにした理由は、「今まで使ったことがなくて使ってみたかったから」ただそれだけです。

    手慣れた MQTT ブローカーサービスがある方はそれをお使いいただいて全く問題ないと思います。

    なお、パブリックなサービスを使うことで、通信手段は Wi-Fi にこだわる必要はなくなりました。
    家の中でも仕事部屋は Wi-Fi が快適に届くけどドアの外は電波が弱い、とか、そもそも自宅に Wi-Fi が無い、なんてこともあるかもしれません。

    というわけで、3G 拡張ボードを使ってセルラー回線による通信を用いることにします。

    さあ、ここまで決まったらあとは作るだけです!

    材料

    用意していただくものはたったのこれだけ!
    簡単・お手軽ですね。

    ※すでに M5Stack をお持ちの方はM5Stack 用 3G 拡張ボードのみもご用意しております。

    構成図

    手順

    この構成を構築する大まかな流れは以下の通りです。

    1. MQTT ブローカーを用意する
    2. PC の Web カメラの使用状況を MQTT ブローカーに Publish する
    3. M5Stack 用のプログラムを開発し、MQTT ブローカーから PC の Web カメラの使用状況を Subscribe する

    では詳細な手順を説明していきます。

    1. MQTT ブローカーを用意する

    構想のところで書いたように、今回は Adafruit IO を使ってみます。
    Adafruit は Arduino などで何かを作ってみたことのある方には馴染みがあるかたも多いのではないかと思いますが、Maker 向けの部品などを販売しているサイトです。
    その Adafruit が提供しているサービスということで IoT デバイスから使いやすい仕上がりになっていることが期待できます。

    アカウントの作成からダッシュボードの作成、MQTT のトピックの作成まで、特に迷うことなくできるのではないかと思います。

    公式サイトの説明

    MQTT のトピックを作ったりそれを可視化するためのダッシュボードを作ったら実際に手元から Publish したりしてみましょう。
    Adafruit IO の画面からもデータを追加したりすることもできます。

    私は手元の環境に MQTT.fx というツールをインストールして試しました。

    2. PC の Web カメラの使用状況を MQTT ブローカーに Publish する

    これはとても簡単で、以下のようなワンライナーのコマンドを実行するだけです。(mosquitto を事前にインストールしておく必要があります)

    lsof /dev/video0 | wc -l | mosquitto_pub -L mqtt://${username}:${key}@io.adafruit.com/${username}/feeds/on-air -s
    

    ${username}${key} は Adafruit IO のアカウントの情報です。

    カメラ未使用時は 0、カメラ使用時は 3(私の環境の場合)という値が送信されることが確認できると思います。

    cron などで定期的に publish するようにしておくとよいでしょう。

    3. M5Stack 用のプログラムを開発し、MQTT ブローカーから PC の Web カメラの使用状況を Subscribe する

    私は以下のようなスケッチを M5Stack で実行するようにしました。
    Arduino IDE から書き込むだけなのでとても簡単です。
    (M5Stack と 3G 拡張ボードは以下の SORACOM Users サイトの内容に従って事前にセットアップしておいてください。)

    プロトタイプ向けマイコンモジュール M5Stack と 3G 拡張ボードをセットアップする

    また、事前に Adafruit MQTT ライブラリをインストール しておく必要があります。

    #include <M5Stack.h>
    
    #define TINY_GSM_MODEM_UBLOX
    #include <TinyGsmClient.h>
    
    #include "Adafruit_MQTT.h"
    #include "Adafruit_MQTT_Client.h"
    
    #define AIO_SERVER "io.adafruit.com"
    #define AIO_SERVERPORT 1883
    #define AIO_USERNAME "USERNAME"
    #define AIO_KEY "aio_XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    #define READ_TIMEOUT 5000
    
    const char MQTT_SERVER[]   PROGMEM = AIO_SERVER;
    const char MQTT_USERNAME[] PROGMEM = AIO_USERNAME;
    const char MQTT_PASSWORD[] PROGMEM = AIO_KEY;
    const char ON_AIR_FEED[]   PROGMEM = AIO_USERNAME "/feeds/on-air";
    
    TinyGsm modem(Serial2); /* 3G board modem */
    TinyGsmClient ctx(modem);
    Adafruit_MQTT_Client mqtt(&ctx, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_USERNAME, AIO_KEY);
    Adafruit_MQTT_Subscribe onAirIndicator = Adafruit_MQTT_Subscribe(&mqtt, ON_AIR_FEED);
    
    void setup() {
      Serial.begin(115200);
      M5.begin();
      M5.Lcd.clear(BLACK);
      M5.Lcd.setTextColor(WHITE);
      M5.Lcd.setTextSize(2);
      M5.Lcd.println(F("M5Stack + 3G Module"));
    
      M5.Lcd.print(F("modem.restart()"));
      Serial2.begin(115200, SERIAL_8N1, 16, 17);
      modem.restart();
      M5.Lcd.println(F("done"));
    
      M5.Lcd.print(F("getModemInfo:"));
      String modemInfo = modem.getModemInfo();
      M5.Lcd.println(modemInfo);
    
      M5.Lcd.print(F("waitForNetwork()"));
      while (!modem.waitForNetwork()) M5.Lcd.print(".");
      M5.Lcd.println(F("Ok"));
      M5.Lcd.clear(BLACK);
      M5.Lcd.setCursor(0,0);
    
      M5.Lcd.print(F("gprsConnect(soracom.io)"));
      modem.gprsConnect("soracom.io", "sora", "sora");
      M5.Lcd.println(F("done"));
      M5.Lcd.clear(BLACK);
      M5.Lcd.setCursor(0,0);
    
      M5.Lcd.print(F("isNetworkConnected()"));
      while (!modem.isNetworkConnected()) M5.Lcd.print(".");
      M5.Lcd.println(F("Ok"));
      M5.Lcd.clear(BLACK);
      M5.Lcd.setCursor(0,0);
    
      M5.Lcd.print(F("My IP addr: "));
      IPAddress ipaddr = modem.localIP();
      M5.Lcd.println(ipaddr);
    
      mqtt.subscribe(&onAirIndicator);
    
      delay(2000);
    }
    
    void loop() {
      M5.update();
    
      MQTT_connect();
    
      Adafruit_MQTT_Subscribe *subscription;
      while ((subscription = mqtt.readSubscription(READ_TIMEOUT))) {
        if (subscription == &onAirIndicator) {
          String lastread = String((char*)onAirIndicator.lastread);
          lastread.trim();
          if (lastread == "") {
            continue;
          }
          if (lastread == "0") {
            showOnAir(TFT_LIGHTGREY);
          } else {
            showOnAir(TFT_RED);
          }
        }
      }
    }
    
    void showOnAir(uint16_t bgColor) {
      M5.Lcd.fillScreen(bgColor);
      M5.Lcd.setCursor(40, 90);
      M5.Lcd.setTextSize(7);
      M5.Lcd.println(F("ON AIR"));
    }
    
    void MQTT_connect() {
      int8_t ret;
      if (mqtt.connected()) {
        return;
      }
    
      M5.Lcd.setTextSize(2);
      M5.Lcd.setCursor(0,0);
      M5.Lcd.print(F("Connecting to MQTT... "));
    
      while ((ret = mqtt.connect()) != 0) {
        M5.Lcd.println(F(mqtt.connectErrorString(ret)));
        M5.Lcd.println(F("Retrying MQTT connection in 5 seconds ..."));
        mqtt.disconnect();
        delay(5000);
      }
    
      M5.Lcd.println("MQTT connected!");
    }
    

    AIO_USERNAMEAIO_KEY はご自身のものに書き換えてください。

    実行

    M5Stack にプログラムを書き込んで実行し、しばらく待つと、以下のような画面に切り替わるはずです(カメラ使用中の場合)

    cron の実行周期にもよりますが、カメラの使用を停止したり再開したりすると、比較的素早く表示が切り替わるのがおわかりいただけるかと思います。

    そのようなことから、MQTT はデバイス側へデータを送ったり指示を出したりしたい時に使いやすいプロトコルであることがご理解いただけるかと思います。

    セキュリティ

    今回は暗号化のない MQTT プロトコルを使いましたが、TLS 暗号化を用いた MQTTS での通信も可能です。

    また、今回は利用しませんでしたがデータ転送サービス SORACOM Beamも MQTT に対応しています。
    この SORACOM Beam を M5Stack と Adafruit IO の間に挟むような構成にすることで「認証情報を M5Stack から無くすことができる(SORACOM に保管できる)」「TLS 暗号化を M5Stack ではなく SORACOM Beam に任せることができる」という2点のメリットがあります。

    SORACOM Beam の活用については 2020年2月に開催された IoT エンジニア向けカンファレンス「SORACOM Technology Camp 2020」の B3 セッション「逆引きIoTクラウドデザインパターン: SORACOMサービスとクラウドサービスの組み合わせ/選択肢を紹介します」も合わせてご覧ください。

    読者の皆様は SORACOM Beam の活用にも是非トライしてみてください。

    まとめ

    私はこの ON AIR サインを今日からさっそく部屋のドアに取り付けて効果があるかどうか見てみたいと思います。

    皆様もお手元のデバイスなどで工夫して作ってみてはいかがでしょうか。

    困難な状況が今後もしばらく続くことが予測されますが、みなさんで知恵を絞ってこの難局を乗り切っていきましょう。

    ogu