Fusic Tech Blog

Fusicエンジニアによる技術ブログ

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

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

みなさまこんにちは。IoTチームの毛利です。

リモートワークしていますか? 私は週4日ぐらいリモートワークです。

とても快適ではあるんですが、夕方になると家族が帰ってくるんです。妻がなにか用事があるときには、そっと仕事部屋に入ってきて、WebMTG中じゃないか様子をうかがっておそるおそる話しかけてきます(ちなみに子どもはお構いなし…)。気を遣わせてしまっているのが申し訳ないなと思って、WebMTG中だということが分かるIoTシステムを作ってみました。

※ 本記事は、2021年 M5Stack アドベントカレンダー 9日目の記事です。

構成

こんな感じです。何の変哲もない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!");
}

//
kta-m

kta-m

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