ABEJA Tech Blog

中の人の興味のある情報を発信していきます

社内フリーアドレス席の予約状況を電子ペーパーで可視化する ~オフィスDXアプリ x M5Paper連携~

こんにちは。2022/09にABEJAに入社したシステムエンジニアの中島です。本記事はABEJAアドベントカレンダー2022の24日目の記事です!

本記事ではBizflexシステムを使用しながら自分が欲しいと思った機能をIoTデバイスのM5Paperで実装してみるという内容ですBizflexシステムでは、クラウドサービスと連携した共用会議室予約システム、取次対応を効率化するゲスト受付システム、社員の所在地・状況可視化システムなどの機能を提供しています。

目的に関連してiOSアプリで使用されるAPIのキャプチャ方法、IoTデバイスの実装を簡単に紹介いたします。また、本記事の内容はBizflexアプリの開発メンバーには一切何も聞かず個人で行っており、ソースコードやバイナリの解析を行わず、外部から観測可能な情報(パケット)のみを使用しております。 そのため、解析内容は実態と異なる可能性があることにご留意いただければ幸いです。

目次

動機

ABEJAのオフィスではBizflexシステムを使用しており、
個人目線ではシステムによって様々な面でオフィスの利便性が向上させているように感じています。
便利な機能だったりは沢山あって是非とも紹介したいのですが、
下記の記事の方が詳しいので気になる方は併せてお読みいただけると嬉しいです。

さて、沢山ある便利機能の中でも最近リリースされたホテリング(フリーアドレスの座席予約)機能が個人的にはとても便利でよく使用しています。
私は基本的にリモートで働いているのですが、
この機能によってオフィスで仕事をする際にもスムーズに着席することができます。

しかし、そんな素晴らしい機能にも一点だけ追加したい事があったりします。
それは現地で自分が予約した席をわかるようにしたいというものです。

予約した人ユーザーの名札のようなものが席の近くにあれば、
Bizflexアプリを開いて確認しなくてもスムーズに着席可能なはずです。 また、間違った席に着席することを防止できます。

原理的にはBizflexシステムで使用しているAPIを使用して情報を取得して、 画面のあるデバイスに予約状況を表示できればよさそうです。

丁度良く、私の手元にはブラックフライデーで購入したM5Paperがあります。
そして、APIの解析に使用するためのPC(過剰スペック)もあります。
必要なものは一通り揃っているので実装を試してみることにしました。

iOSのBizflexアプリで使用されているAPIをキャプチャする

環境準備

Bizflexシステムでは画面に状況を表示するために何らかのAPIを使用している可能性が高いです。 そして、セキュリティに考慮しているならばAPIはHTTPSのはずです。

PCのHTTP通信をキャプチャするならばWireshark等がありますが、 今回はiOSデバイスのHTTPS通信を簡単にキャプチャしたいのでCharles Proxyを使用します。

PC(Windows)へのインストール方法は下記が参考になります。 helpx.adobe.com

PCへインストール/セットアップが完了したら、
iOSデバイス側でCharles Proxyをプロキシとして利用するよう設定します。 手順は下記です。

  1. Charles ProxyをインストールしたPCのIPアドレスを確認する。
    PC(Windows)のターミナルでipconfigコマンドを使用してIPアドレスを取得します。
  2. iOSデバイスでWifiのプロキシを設定する。
    ※ PCとiOSデバイスは同一のネットワークに接続してください。
    1. [設定] > [Wi-Fi] > [Wifi詳細] > [HTTPプロキシ] > [プロキシを構成]を押下する。
    2. プロキシを手動に切り替え下記の設定を行い[保存]を押下する。
      サーバ: 上記で取得したPCのIPアドレス
      サーバ: 8888
      認証: オフ
  3. iOSデバイスでCharles Proxyのルート証明書をインストールする。
    iOSデバイスからhttp://www.charlesproxy.com/getsslにアクセスします。
    画面の指示に従って証明書のインストールを行います。
  4. iOSデバイスでCharles Proxyのルート証明書を信頼する。
    [設定] > [一般] > [情報] > [証明書信頼設定]からCharles Proxyの証明書をオンにします。

上記で設定が完了しました。
設定を正しい場合はiOSデバイスでSafariでブラウジングを行うと、
PCのCharles Proxy上にパケットの内容が表示されます。

解析

ここまでで解析の準備が整いました。
それでは実際にBizflexアプリを使用して内部で使用されているAPIを見ていきましょう。

今回私が欲しいのは下記のAPIです。 対象のAPIが存在するかを重点的に見ていきます。

  • ある座席に対しての予約状況を取得するAPI
  • (必要あれば)予約を行っているユーザーの情報を取得するAPI

また、併せて認証がどのように行われているかもチェックしていきましょう。

Charles Proxyに接続した状態でBizflexアプリを起動します。

まずは認証について確認です。
Bizflexアプリを起動してCharles Proxyで確認します。
APIをキャプチャできるか半信半疑でしたが、実際に使用されていることがわかりました。

APIのリクエストを確認すると、認証はAuthorizationヘッダを使用していることがわかります。
OAuth2.0の認可コードフローのようにもみえます。
試しに、AuthorizationヘッダをコピーしてcurlでAPIのGETリクエストをしてみて成功しました。
以上から、下記流れであれば外部からでもAPIで情報取得することができそうです。

  1. トークン取得APIでトークン発行
  2. 上記で取得したトークンで各種APIで情報取得

APIの流れがわかったところで、今回の実装では自動化できない部分が存在することがわかりました。
Bizflexアプリのトークン取得APIを使用する段階でログインを求められます。
ABEJAではログインに多要素認証を取り入れているため、
残念ながらトークン取得のみは手作業で行う必要がありそうです。
※ その部分を自動化を試すのはセキュリティ的にもよろしくないので実行しません。(情シスの方にも怒られそうです。)

トークン取得に関しては残念だったものの、
動作をみながらAPIを確認し使用するAPIを下記2つに決めました。
解析をする時にはAPIが綺麗に作られている程、動作が解りやすくていいですね。

  • ある座席に対しての予約状況を取得するAPI
    /reservations/areas/{エリアID}/groups/{グループ名}/seats/{シート番号}
    -> 予約状況の情報にはユーザーIDのみでユーザー名がないためユーザー情報は別APIで取得します。
  • ユーザーIDからユーザー情報を取得するAPI
    /organizations/{会社ID}/users/{ユーザーID}

M5PaperでBizflexのAPIと通信して予約状況を表示する。

使用するAPIを決めたので、ここからはM5Paperの開発を行います。
今回はVisual Studio Code(以下、VSCode)に拡張機能でPlatformIOをインストールして開発を行います。

環境準備

  1. M5Paper用にドライバをインストールする。
    M5Stackのページからドライバをダウンロードします。
    https://docs.m5stack.com/en/quick_start/m5paper/arduino
    ダウンロードしたインストーラの指示に従ってインストールを行います。

  2. VSCodeにPlatformIO拡張機能をインストールする。
    VSCodeのExtenionタブでplatformioと入力して検索します。
    PlatoformIO IDEという拡張機能があるのでインストールします。

M5Paper用のプロジェクトを作成する

今回用にプロジェクトを作成します。

  1. VSCodeのPlatformIO拡張からHomeを押下する。
  2. NewProjectを押下し、下記の設定でプロジェクトを作成する。
    • Name: HelloTest(なんでも良いです)
    • Board: M5Stack Fire
    • Framework: Arduino

ここまででプロジェクトを作成できたので、ライブラリのインストールと必要な設定を行います。

  1. VSCodeのPlatformIO拡張からLibrariesを押下する。
  2. M5EPDを検索しインストールする。
  3. ArduinoJsonを検索しインストールする。

  4. Serialが文字化けする場合があるので、プロジェクトのplatformio.iniに下記設定を追加する。
    monitor_speed = 115200

テストでM5Paperにプログラムを書き出します。
以降の手順でも書き出しが必要な場合はこの手順を行っています。

手順の準備としてmain.cppに下記を記述します。

#include <M5EPD.h>

M5EPD_Canvas canvas(&M5.EPD);
void setup(){
  M5.begin();
  M5.EPD.SetRotation(90);
  M5.EPD.Clear(true);
  M5.RTC.begin();
  canvas.createCanvas(540, 960);
  canvas.setTextSize(3);
  canvas.drawString("Hello World", 45, 350);
  canvas.pushCanvas(0,0,UPDATE_MODE_DU4);
}

void loop(){
}

※参考元: https://docs.m5stack.com/en/quick_start/m5paper/arduino

  1. PlatformIOのUpload and Monitorを押下する。
  2. 一定時間後に、M5Paper上にHello worldが表示されれば成功です

M5PaperでWifiに接続する

WiFi.hを利用します。 main.cppに下記を記述します。

#include <M5EPD.h>
#include <WiFi.h>

const char *ssid = "{WifiのSSID}";
const char *password = "{Wifiのパスワード}";

M5EPD_Canvas canvas(&M5.EPD);

void setup()
{
  M5.begin();
  M5.EPD.Clear(true);
  M5.RTC.begin();
  canvas.createCanvas(960, 540);

  // Wifi接続処理
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.println("WiFi not connected");
    delay(500);
  }
  Serial.println("WiFi connected");
  Serial.flush();
}

void loop()
{
}

上記をM5Paperに書き出します。
Wifi接続に成功すれば、SerialにWifi Connectedが表示されます。

M5PaperでAPIにアクセスする

HTTPClient.hを利用します。 main.cppの先頭に下記を追記します。

#include <HTTPClient.h>

main.cppのloop処理に下記を追記します。

  HTTPClient http;
  String payload;
  
  http.begin("{APIのURL}");

  http.addHeader("Authorization", "Bearer {Bizflexアプリから取得したトークン}");

  int httpCode = http.GET();
  if (httpCode > 0)
  {
    if (httpCode == HTTP_CODE_OK)
    {
      payload = http.getString();
      Serial.println(payload);
      Serial.flush();
    }
  }
  http.end();
  delay(1800 * 1000);

上記をM5Paperに書き出します。
API取得に成功すれば、Serialにレスポンスが表示されます。

APIレスポンスをパースする

ArduinoJson.hを利用します。 main.cppの先頭に下記を追記します。

#include <ArduinoJson.h>

先程の関数に下記を追記します。

  HTTPClient http;
  String payload;
  DynamicJsonDocument doc(4096); //追記部分

  http.begin("{APIのURL}");

  http.addHeader("Authorization", "Bearer {Bizflexアプリから取得したトークン}");

  int httpCode = http.GET();
  if (httpCode > 0)
  {
    if (httpCode == HTTP_CODE_OK)
    {
      payload = http.getString();
      Serial.println(payload);
      Serial.flush();

      deserializeJson(doc, payload); //追記部分
      String data = doc[{レスポンスのパラメータ}]; //追記部分。パースしたい内容を記述
      Serial.println(data); //追記部分
      Serial.flush(); //追記部分
    }
  }
  http.end();
  delay(1800 * 1000);

上記をM5Paperに書き出します。
パースに成功すれば、Serialにパースしたレスポンスが表示されます。

全体

必要な要素は抑えたので今回は下記の流れで予約状況を表示します。

[init処理]

  1. 各種初期化。
  2. Wifiへの接続を行う。

[loop処理]

  1. 時刻取得。
  2. 該当時刻の予約状況取得。(Bizflex API)
  3. 予約ユーザーの情報取得。(Bizflex API)
  4. 画面表示

上記より、main.cppは下記のようになりました。

#include <M5EPD.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>


const char *ssid = "{WifiのSSID}";
const char *password = "{Wifiのパスワード}";
const char *ntpServer = "ntp.nict.jp";
const char *token = "{Bizflexアプリから取得したトークン}";

const long gmtOffsetSec = 9 * 3600;
const int daylightOffsetSec = 0;

HTTPClient http;
M5EPD_Canvas canvas(&M5.EPD);
DynamicJsonDocument doc(4096);

// APIリクエストを簡単に行えるよう関数を作成しています。
std::tuple<int, String> httpGet(const char *url, const char *token)
{
  String authorization("Bearer ");
  String payload;

  http.begin(url);

  authorization.concat(token);
  http.addHeader("Authorization", authorization);

  int httpCode = http.GET();
  if (httpCode > 0)
  {
    if (httpCode == HTTP_CODE_OK)
    {
      payload = http.getString();
    }
    else
    {
      // リトライ処理が必要であればここで実装してください
    }
  }
  else if (httpCode <= 0)
  {
    // リトライ処理が必要であればここで実装してください
  }

  http.end();

  return std::forward_as_tuple(httpCode, payload);
}

void setup()
{
  M5.begin();
  M5.EPD.Clear(true);
  M5.RTC.begin();
  canvas.createCanvas(960, 540);

  // Wifi接続処理
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.println("WiFi not connected");
    delay(500);
  }
  Serial.println("WiFi connected");
  Serial.flush();

  configTime(gmtOffsetSec, daylightOffsetSec, ntpServer);
}

void loop()
{
  struct tm timeinfo;
  if (getLocalTime(&timeinfo))
  {
    int code;
    String payload;
    char url[200];

    String userName = "Vacant seat";

    // 予約状況取得API
    // start, endのクエリーで取得する時刻を指定しています。
    sprintf(url, "https://api.bizflex.app/reservations/areas/{エリアID}/groups/{グループ名}/seats/{シート番号}?start=%04d-%02d-%02dT%02d%%3A00&end=%04d-%02d-%02dT%02d%%3A59", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour);
    std::tie(code, payload) = httpGet(url, token);
    deserializeJson(doc, payload);

    String userId = doc["reservations"][0]["userId"];

    if (!userId.equals("null"))
    {
      // ユーザー情報取得API
      sprintf(url, "https://api.bizflex.app/organizations/abejainc-com/users/%s", userId.c_str());
      std::tie(code, payload) = httpGet(url, token);
      deserializeJson(doc, payload);

      userName = (const char *)doc["name"];
    }

    // 画面にユーザー名を表示します。
    canvas.fillCanvas(0);
    canvas.setTextSize(20);
    canvas.drawString(userName, 40, 240);
    canvas.pushCanvas(0, 0, UPDATE_MODE_GLR16);
  }

  // 30分休止します。
  delay(1800 * 1000);
}

上記をM5Paperに書き出します。
予約状況から私の名前を取得して表示することができるようになりました。

まとめ

今回は自分が欲しい機能のプロトタイプを実装してみました。
いつも利用しているシステムのAPIを直接触ることより、
システムへの理解と愛着が一層深まりました。
今後も機会があれば身の回りのものを解析して便利にできるか試してみようと思います!

今回のプロトタイプで出来なかったことや追加構想を下記にまとめます。

[出来なかったこと]

  • トークン取得を自動化できなかったこと
    -> 多要素認証が必要な社用アカウントへIoTデバイスで自動的にのログインするのはセキュリティ的によくないかと思うので、セキュリティ的に問題ない方法でトークン取得可能かは別途検討します。

[追加構想]

  • 空席状態の場合、M5Paperでタッチパネルを押すと座席の予約ができるようにする。
    -> この場合でも、個人のトークンをどのように取得するか検討が必要そうです。
  • Wifiオフやライトスリープ、ディープスリープを利用して無給電での動作時間の延長をする。
  • 日本語フォントにも対応する。
  • M5Paperより安価で同様の操作を行えるようなデバイスに実装する

We Are Hiring!

ABEJAでは一緒に働く仲間を募集しています!
幅広い技術を活用・習得したい、顧客・社内メンバーと協業しながら開発したい、このブログを読んで興味を持った方は是非こちらの採用ページからエントリーください!

careers.abejainc.com