Top View


Author kta-m

WebMTG中なのを家族に自動で示すIoTシステムを作ったらリモートワークが快適になったよ

2021/12/09

構成

こんな感じです。何の変哲もないIoTですね。 AWS IoT Coreを介して、WebMTG中かどうかのステータスを渡します。

webmtg-notify-architecture

状態表示は何でもいいんですが、手元にあったAtom Matrixを使用しました。 意外といろいろできるんですが、これが2,000円もしないとは驚きです。

AWS IoT Coreの設定

今回は単にMQTTのブローカーとして使うだけなので、ポリシーと証明書のみ設定します。

ポリシーを作成して、

webmtg-notify-aws-iot-core-create-policy

1-Click 証明書作成で証明書を作成して、

webmtg-notify-aws-iot-core-create-cert

各種ファイルをダウンロードして(ルートCAファイルはRSA2048ビットキー: Amazon Root CA 1)、証明書を有効化して、

webmtg-notify-aws-iot-core-create-cert2

ポリシーをアタッチして完了です。

webmtg-notify-aws-iot-core-attach-policy

WebMTG中かどうかを判定・送信する(mac)

WebMTGを始めるときに手動でボタンを押すとかすると絶対に押し忘れる自信があるので、どうにかして自動で判定したいですね。

目をつけたのはlsofコマンドです。WebMTGツールは映像や音声の送受信にUDPを使っているので、lsofコマンドで利用されているUDPポートの一覧を出し、そこからWebMTG系のプロセスに絞り込むことができればOK、という寸法です。

Slack(call/huddle), Teams, Zoom, Google Hangout, DiscordでWebMTGを始めたときにどんな変化があるのかを調べまして、 コマンド的には以下で絞り込めることがわかりました。 (自分の環境でしか試してないので、他の環境全てで大丈夫かは分かりませんが。。)

# Slack call/huddle, Zoom, Teams, Google Hangout
lsof -iUDP | grep -e Slack -e zoom\.us -e Microsoft -e Google | grep -E '192\.168\.0\.[0-9]+:[0-9]+$'
# Slack huddle
lsof -iUDP | grep Slack | grep -E '10\.0\.0\.[0-9]+:[0-9]+$'
# Discord
lsof -iUDP | grep Discord | grep -E '\*:[0-9]+$'

これを定期的にAWS IoT Coreに通知するアプリをGoで書きました。 Githubで公開していますので詳細はこちらを参照してください。

メインのWebMTG中か判断する部分はこちらです。

func GetStatus() bool {
	return getStatusByTools([]string{"Slack", "zoom\\.us", "Microsoft", "Google"}, "grep -E 192\\.168\\.0\\.[0-9]+:[0-9]+$") ||
		getStatusByTools([]string{"Slack"}, "grep -E 10\\.0\\.0\\.[0-9]+:[0-9]+$") ||
		getStatusByTools([]string{"Discord"}, "grep -E '\\*:[0-9]+$'")
}

func getStatusByTools(toolNames []string, optionalGrep string) bool {
	cmdstr := "lsof -iUDP | grep"
	for _, name := range toolNames {
		cmdstr += fmt.Sprintf(" -e %s", name)
	}
	cmdstr += " | "
	cmdstr += optionalGrep
	cmdstr += " | wc -l"
	fmt.Println(cmdstr)

	out, err := exec.Command("sh", "-c", cmdstr).Output()
	if err != nil {
		log.Println(err)
		return false
	}

	num, _ := strconv.Atoi(strings.TrimSpace(string(out)))

	return num > 0
}

ただ、これだと一つ問題があります。ChromeはたまにUDPで何か通信しているらしく、これがGoogle HangoutのUDP通信と判断がつかないのです。。 なので、10秒ごとにステータスを見て、2回連続trueになればWebMTGが開始したと判定するようにしました。 逆に、終了判定は即時で、ステータスがfalseになればすぐにWebMTG終了のメッセージを送っています。

こんな感じ↓

	history := []bool{false, false, false}
	for {
		status = webmtg_status.GetStatus()
		log.Printf("status: %v\n", status)

		history = append([]bool{status}, history[:2]...)
		log.Printf("history: %v\n", history)

		if history[0] && history[1] && !history[2] {
			// 2連続trueでtrueを送信
			sendStatus(client, topic, true)
		} else if !history[0] && history[1] && history[2] {
			// falseになれば即falseを送信
			sendStatus(client, topic, false)
		}

		time.Sleep(time.Duration(interval) * time.Second)
	}

あとはこれをMQTTでAWS IoT Coreに送ってあげます。

func Connect(clientId string, endpoint string, rootCAPath string, certPath string, keyPath string) (mqtt.Client, error) {
	tlsConfig, err := newTLSConfig(rootCAPath, certPath, keyPath)
	if err != nil {
		return nil, fmt.Errorf("failed to construct tls config: %v", err)
	}
	opts := mqtt.NewClientOptions()
	opts.AddBroker(fmt.Sprintf("ssl://%s:%d", endpoint, 443))
	opts.SetTLSConfig(tlsConfig)
	opts.SetClientID(clientId)
	client := mqtt.NewClient(opts)
	if token := client.Connect(); token.Wait() && token.Error() != nil {
		return nil, fmt.Errorf("failed to connect broker: %v", token.Error())
	}

	return client, nil
}

func Disonnect(client mqtt.Client, quiesce uint) {
	client.Disconnect(quiesce)
}

func Publish(client mqtt.Client, topic string, qos byte, retained bool, payload interface{}) error {
	token := client.Publish(topic, qos, retained, payload)
	token.Wait()
	if err := token.Error(); err != nil {
		return fmt.Errorf("failed to publish %s: %v", topic, err)
	}
	return nil
}

func newTLSConfig(rootCAPath string, certPath string, keyPath string) (*tls.Config, error) {
	rootCA, err := ioutil.ReadFile(rootCAPath)
	if err != nil {
		return nil, err
	}
	pool := x509.NewCertPool()
	pool.AppendCertsFromPEM(rootCA)
	cert, err := tls.LoadX509KeyPair(certPath, keyPath)
	if err != nil {
		return nil, err
	}
	cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
	if err != nil {
		return nil, err
	}
	return &tls.Config{
		RootCAs:            pool,
		InsecureSkipVerify: true,
		Certificates:       []tls.Certificate{cert},
		NextProtos:         []string{"x-amzn-mqtt-ca"}, // Port 443 ALPN
	}, nil
}

WebMTG中かどうかを受信・表示する(Atom Matrix)

これはMQTTで特定のTopicをサブスクライブしておいて、受信した内容に応じて表示を切り替えるだけですね。 MTG中は赤、それ以外は緑になるようにしています。

こちらもGithubで公開していますので詳細はこちらを参照してください。 secret.h.templateに自身の環境の設定項目を書いて、secret.hにすると動くと思います。

#define ESP32

#include <M5Atom.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "Secret.h"

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
static WiFiClientSecure httpsClient;
static PubSubClient mqttClient(httpsClient);

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// WiFi接続
void setupWifi() {
  int count = 0;

  Serial.println("Wifi Connecting...");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    count++;
    int status = WiFi.status();
    delay(1000);
    if (count % 10 == 0 && (status == WL_DISCONNECTED || status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST || status == WL_NO_SSID_AVAIL)) {
      Serial.println("Reconnect...");
      WiFi.disconnect();
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    }
    if (count >= 30) {
      Serial.println("Restart");
      ESP.restart();
    }
    Serial.print(status);
  }
  Serial.println("");
  Serial.println("Wifi Connected!");
}

//-----------------------------------------------------------------------------
// MQTT接続のセットアップ
void setupMqtt() {
  httpsClient.setCACert(ROOT_CA);
  httpsClient.setCertificate(CERTIFICATE);
  httpsClient.setPrivateKey(PRIVATE_KEY);
  mqttClient.setServer(CLOUD_ENDPOINT, CLOUD_PORT);
  mqttClient.setCallback(mqttCallback);
}

//-----------------------------------------------------------------------------
// MQTT接続
void connectMqtt() {
  Serial.println("MQTT Connecting...");
  while (!mqttClient.connected()) {
    if (mqttClient.connect(CLIENT_ID)) {
      mqttClient.subscribe(CLOUD_TOPIC, 0);
    } else {
      delay(1000);
      Serial.print(".");
    }
  }
  Serial.println("MQTT Connected!");
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// MQTTサブスクライブ
void mqttCallback (char* topic, byte* payload, unsigned int length) {
    Serial.print("Received. topic=");
    Serial.println(topic);

    char message[length];
    for (int i = 0; i < length; i++) {
      message[i] = (char)payload[i];
    }

    DynamicJsonDocument doc(200);
    deserializeJson(doc, message);
    String status = doc["status"];
    Serial.print("status:");
    Serial.println(status);

    if (status == "true") {
       M5.dis.fillpix(0xff0000);
    } else {
       M5.dis.fillpix(0x00ff00);
    }
}

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
void setup()
{
    M5.begin(true, false, true);
    delay(10);

    Serial.begin(9600);
    delay(10);

    M5.dis.fillpix(0xffffff);
    M5.dis.setBrightness(50);

    setupWifi();
    setupMqtt();
}

//-----------------------------------------------------------------------------
void loop()
{
    if (!mqttClient.connected()) {
        connectMqtt();
        M5.dis.fillpix(0x0000ff);
    }
    mqttClient.loop();

    delay(50);
    M5.update();
}

設置!

案外これがなかなか難しいです。 ちょうどいい場所にコンセントもないし、かといってバッテリーにすると電池のメンテナンスが大変だし。。とりあえず暫定で置いてみました。それでも結構好評です。

インターホンや給湯器のコントローラーがあるあたりにコンセントがあれば、各種IoTシステムのモニターもそこに置けていいなと思うんですが。。これから家を建てる人はぜひご検討ください。

まとめ

とても簡単な仕組みながら、日頃の生活を一つ便利にすることができました。 WebMTGかどうかの状況をAWS IoTまで送ることができたので、使い方によってはWebMTG中にSlackのステータスを変えるなどの拡張もできそうです。

ただ、これを誰でも使えるようにしようと思ったら、mac側のアプリの配布やプロセスの自動起動、windows対応、MQTTの接続が切れたときの処理、wifi接続の仕組み、業務時間外は表示をOFFにする機構、isofでのチェック条件の更新機構などなど、やることが山積みです。大変だー。

kta-m

kta-m

先進技術部門 IoTチーム チームリーダー