さくらのIoT Platformで、”ヤバい状況であることを光って知らせる装置”をできるだけ安価に作ったお話

さくらインターネットでは「新しい働き方」というのを常に模索しているのですが、その中の一つに「フリーアドレス(自席を設けず、好きな場所で仕事ができる)の実現」が候補として挙がっています。

今回はその時に問題として議題に出ていた項目の一つにおいて、個人的に試してみた内容を取り上げてみたいと思います。

ちなみに前回と同様、社内向けに公開したものを再編集しています。その関係上、一部ぼかした表現となる箇所がありますので、あらかじめご了承ください。

それではどうぞ。

背景

大阪本社の移転に伴い、フリーアドレスの導入が検討されているものの、「アラートの扱いどうするの?」というネタが俎上に載せられていた。

※監視するしくみについて大雑把に説明すると以下のようになります。※通知する手段は他にもいろいろあります。

問題点はだいたいこんな感じ↓

  • 音が鳴るので業務に関係しない人には耳障りに感じるかも
  • 警報器のそばにいないと光っていることに気づけない

出てきた案

  • ブザーが耳障りなら何か音楽や効果音に変えるとか、机のパーティションが光るとか。
  • そもそも警報器が要らん
    • →管理対象が多いことから頻繁に点灯しており、「異常である事が平常」のようになっているため
      ※サービス全体で見ると稼働率は99.99%以上あります
    • →ウェアラブルデバイス(例えばApple WatchやAndroid Wear)へバイブレーションで通知すれば済むのではないか

個人的に警報器は検知というより、マズい状況であるという雰囲気を周囲に知らせるのに適したツールでもあると理解していたので、廃止するとそれはそれで現場の緊張感みたいなのを共有できなくなるんで困るような気がする。警報器があれば「今取り込み中だから静かにして」て言う必要も無いし。でも音を鳴らすのは問題だという意見は一理あるしなぁ、と。

うーんうーn

考え方を変えてみる

警報器的な役割を持つ機器を、好きなタイミングで簡単に操作できる仕組みが用意できれば解決できるかもしれんと思いつく。
そういやちょうどArduinoを触ったばっかだし、テープLEDを使えば音を立てずにさりげなく通知できそうな気がする。よし、やってみよう!

どうやって作ろうか

やってみようと思ったまでは良いものの、「さてどうしたものか」とさっそく行き詰まる。絵に描いた餅どころか餅の絵が描けない。
でも無い知恵を無理やり絞って餅の絵を描いてみる。

餅1. Arduinoを使ってみる

ArduinoにWiFiモジュールを追加して、サーバ監視ツールのステータスが変わったら通知してみたらどうかとひらめく。
でも、Arduinoにどうやって知らせるのか?執務エリアにあるネットワークにどうやって入るのか?を考えたとたん、悲しい気持ちになる。この案はボツ。

※餅1

※社内のネットワークはサービス提供用のネットワークと異なっており、執務エリアで利用するPC等の端末はプライベートIPアドレスが割り振られています。また、データセンターにあるサーバから社内のネットワークへアクセスすることはポリシー的に許可されていません。

餅2. Raspberry piを使ってみる

Raspberry piにサーバ監視ツールのステータスを見るプログラムを入れてみたらどうかとひらめく。
でも「管理するサーバが増えるのか」と思うと気が滅入る。とはいえ先の案よりは現実的なんでキープしつつ、次。

※餅2

餅3. Arduino+さくらのIoTを使ってみる

漠然と考えてはいたものの、「なんでさくらのIoTなの?無理して使おうとしてない?ひょっとしてミーハー?」とか言われたら泣きそうになること必至だったので意識しないようにしていたのに、先の2つの餅と比較して、「社内ネットワークの構成に関与しない」「メンテナンスが楽」というメリットに気づく。

※餅3

ということで絵に描いた餅3を動く餅にしてみよう。できるかどうかわからんけど。

もう少し具体的に考えてみる

用意する装置の再確認

サーバ監視ツールのステータスを見るプログラム

HTMLを読み込んでそれっぽい文字列が無いかを探って、その結果に応じてArduinoへ命令を送信するプログラムを書こう!根性で。

Arduino+さくらのIoT

前回はDataStore(β)を使ったけど、今回はIoTモジュールへデータを送信することになるので、別の方法を考えないとダメっぽい。
WebScoket(双方向通信)を使っても良いとは思うものの、せっかくならIncoming Webhook(IoTモジュールへの送信)を使ってみようかな。

※さくらのIoTモジュールと通信する手段は複数あります。詳細については【公式ドキュメント】をご覧ください。

テープLED

テープLEDを使えば面白いことができそうなのはなんとなくわかっていたものの、今まで食指が向かず、行動を起こさずにいた。

55" Raspberry Pi Ambilight - 222 LEDs (WS2812b)
https://www.youtube.com/watch?v=ELmKBTcXoac

詳しく調べてみたところ、「ws2812b」という製品はLEDライトにマイコンを内蔵していて、細かいことを考えなくても使えそうな感じであることが判明。身銭をきって以下の2つをポチる

[一つ目]
ディアイワイモール (DIYmall) WS2812 5050 RGB 12ビット LED ランプ モジュール 集成ドライバー Arduino適用

https://www.amazon.co.jp/dp/B01LXAGO88/


テスト用LED

コレはテスト用なので実際には使わないかもしれんけど頑張ってはんだを付ける。昔ついカッとなって買ったものの、お蔵入りしていた電子工作キット(の一部)がようやく日の目を見る。祝。

[二つ目]
ALITOVE WS2812B 個別アドレス可能 LEDテープライト 5050 RGB SMD 1 m 60個ピクセル夢色 防水 黒いベース 5V DC

https://www.amazon.co.jp/dp/B01N0KBB5C/

テープLED

あと、円形のブツはオス-メスのジャンパワイヤー、テープLEDはオス-オスのジャンパワイヤーを用意。

テープLEDのコネクタはジャンパワイヤーで接続。

とりあえず試すだけならコレでOK

いざ作成

1. テープLEDをArduinoから光らせる。

まずはArduinoでLEDを光らせてみる。ドライバをArduinoIDEへインストールし、サンプルを実行。

GitHub - adafruit/Adafruit_NeoPixel: Neo Pixels!
https://github.com/adafruit/Adafruit_NeoPixel

ちょ、まぶしい!派手すぎ!こんなんあかんやんけ!ピカピカしすぎやわ!

気を取り直してサンプルのソースを眺め、使えそうなところだけいただく。

2. さくらのIoTモジュールへ命令を送る

Incoming Webhook(IoTモジュールへ送信する手段)を使おうと決めたまでは良いけど、やり方がわからん。
APIのマニュアルを見てあれこれと試すものの、どれも失敗。むー

仕方が無いんでIoT事業部の西田さんへ泣きついてみるも、やはりうまくいかない。その後2日ほど色々試してみるけど全滅。

  • APIのマニュアルにはPOSTのほかにPUTというメソッドを使ったサンプルも書いてあった→×
  • サービス(WebSocketやWebhook)から発行されるトークンではなく、APIキーに発行されているトークンを使ってみる→×
  • WebSocketにしてみる→×
  • And more...

あーあーと思っていたところ、西田さんから「コレで試してみ」と見本を提示してもらう。でも失敗。

なんでやねんと思いつつ、自身の設定を見ると「secret」という項目へ値を入れていることに気づく。
コンパネに設定した値とjsonにて設定している値を消してからリトライ→できた。

正直「うわー・・・しょーもな」と思う気持ちは半分ほどあるものの、それでも感謝!超感謝!!ありがとう西田さん!チュッチュッ!!←キモい

※【重要】後述しますがSecretの設定は可能です。

思いのほか時間を消費したけど、これで前に進めるぜ!と気持ちを切り替える。

APIのマニュアルはコマンドラインからcurlを使ったサンプルだったので、IoTモジュールへのデータ送信はシェルスクリプトで良いかなとか一瞬魔が差したものの、後々のことを考えると他の言語のほうがよさそうと思いなおす。

「前回はPHPで作ったよな?今回はPython使ってみなよオッサン」という心の声に従い、軽く試してみるも、思いのほかイメージどおりにできなかったのであっさり断腸の思いであきらめる。大事に温めようこの想い。
ってことで今回もPHPで作成。

完成

苦労話やらポエムばかり書いても何なので、出来上がったものを以下に記します。何かあればご指摘ください。

Arduinoのスケッチ

保存名は任意でOK

#include <SakuraIO.h>
#include <Adafruit_NeoPixel.h>
SakuraIO_I2C sakuraio;
#define LED_1 13
#define RGBLED_OUTPIN    2           // 信号制御用。使用するGPIOソケットにあわせて適宜変更。A0に挿すなら14を指定
#define NUMRGBLED        60          // LEDの個数(テープLEDは60。円形のブツは12)
Adafruit_NeoPixel RGBLED = Adafruit_NeoPixel(NUMRGBLED, RGBLED_OUTPIN, NEO_GRB + NEO_KHZ800);
uint32_t cnt = 0;

void setup() {
  RGBLED.begin() ;                   // RGBLEDのライブラリを初期化
  RGBLED.setBrightness(80) ;         // 明るさの指定(0-255)
  RGBLED.setPixelColor(150, 150, 150, 0) ; // 適度に明るい色
  RGBLED.show() ;                    // LEDにデータを送り出す
  delay(1000);
  Serial.begin(9600);
  Serial.print("Waiting to come online");
  for (;;) {
    if ( (sakuraio.getConnectionStatus() & 0x80) == 0x80 ) break;
    Serial.print(".");
    delay(1000);
  }
  Serial.println("");
  pinMode(LED_1, OUTPUT);
}

void loop() {
  uint8_t available;
  uint8_t queued;
  boolean signal_flg;
  boolean blink_flg;
  cnt++;
  Serial.print("Count :");
  Serial.print(cnt);
  available = 0;

  if (sakuraio.getRxQueueLength(&available, &queued) != CMD_ERROR_NONE) {
    Serial.println("[ERR] get rx queue length error");
  }

  if (available > 0){
    uint8_t ch, type, value[8];
    int64_t offset;
    sakuraio.dequeueRx(&ch, &type, value, &offset);
    Serial.print("  Value :");
    Serial.println(value[0]);
    if (value[0] == 1) {
      signal_flg = 1;
    }else{
      signal_flg = 0;
    }
  }

  if(signal_flg == 1){
    digitalWrite(LED_1, HIGH);
    colorWipe(RGBLED.Color(127, 0, 0), 1, 25); // Red
    blink_flg = 1;
  }else{
    digitalWrite(LED_1, LOW);
    colorWipe(RGBLED.Color(10, 10, 127), 1, 25); // Blue
    blink_flg = 0;
  }
  sakuraio.clearRx();
  delay(1200);
  if(blink_flg == 1){
    colorWipe(RGBLED.Color(0, 0, 0), 1, 25); // Tone down.
  }
  delay(1200);
}

void colorWipe(uint32_t c, uint8_t wait, uint8_t b) {
  for(uint16_t i=0; i<RGBLED.numPixels(); i++) {
    RGBLED.setBrightness(b) ;
    RGBLED.setPixelColor(i, c);
    RGBLED.show();
    delay(wait);
  }
}

補足1:点滅する処理をさせたくて、loopの中にfor文を書き、その中にdelayを挟んだけど、getRxQueueLengthに失敗した。仕方がないんでfor文を外し、代わりにフラグを見て判定させる処理に変更した。
補足2:LEDの輝度を変えたい場合「colorWipe」の第3引数を100くらい上げてみよう(最大値は255)

サーバ監視ツールのステータスを見るプログラム

check.phpという名称で保存

<?php
ini_set('mbstring.internal_encoding', 'UTF-8');
$url = "データ取りたいところのURL";
while(1){
  $status = 1;
  $ch = curl_init();
  // 通信したいサーバによって適宜変更のこと
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
  curl_setopt($ch, CURLOPT_FORBID_REUSE, TRUE);
  curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
  $data = curl_exec($ch) or die('read error');
  curl_close($ch);
  $data = mb_convert_encoding($data, 'utf-8', 'auto');
  $line = explode("\n", $data);

  for($i=0; $i<count($line); $i++){
    $line[$i] = trim($line[$i]);
    if(preg_match('/取りたい文字列/', $line[$i])){
      //echo $line[$i] . "\n";
      $status = 0;
    }
  }

  if($status === 0){
    echo $status . "\n";
    exec('php send_incoming_webhook.php 0');
  }else{
    echo $status . "\n";
    exec('php send_incoming_webhook.php 1');
  }

  sleep(30);
}

補足1:サーバ監視ツールのステータスへアクセスできるサーバか端末に設置すればOK。30秒に一回更新。
補足2:phpからphpをexecで呼び出しているのはご愛嬌

さくらのIoTモジュールへ信号を送るプログラム

send_incoming_webhook.phpという名称で保存

<?php

$digit = $argv[1];
$module = "モジュールID";
$token = "トークン";
$secret = "探さない、待つの。"; //コンパネで設定した任意の値

if(preg_match('/[^0-9]/', $digit)) Usage($argv[0]);

function Usage($arg){
  echo "Usage: $arg intval(num) \n";
  exit;
}

function send_to_iot($token, $payload, $secret){
  //$webhook_url = 'https://api.sakura.io/incoming/v1/' . $token;
  $webhook_url = "https://api.sakura.io/incoming/v1/{$token}";
  $x_sakura_signature = hash_hmac("sha1", $payload, $secret);
  $headers = array(
    'Content-Type: application/json',
    'Accept: application/json',
    'X-Sakura-Signature: '. $x_sakura_signature,
  );
  $options = array(
    'http' => array(
      'method' => 'POST',
      'header' => implode("\r\n", $headers),
      'content' => $payload,
    )
  );
  //var_dump($options);
  $response = file_get_contents($webhook_url, false, stream_context_create($options));
  return $response === 'ok';
}

$data = array(
  'type' => 'channels',
  'module' => $module,
  'payload' => array(
    'channels' => array( array(
        'channel' => 0,
        'type' => 'i',
        'value' => intval($digit),
      )
    )
  )
);
//var_dump($data);
$payload = json_encode($data);
send_to_iot($token, $payload, $secret);

※このソースはSecretに対応しています。値はコントロールパネルにて任意の値を先に入力しておいてください。

※ポエムや愛の告白を文字に託してもみるのも良いですね

動作の流れ

[check.php] → execで呼び出し → [send_incoming_webhook.php] → さくらのIoT Platform → [Arduino]

使い方

check.phpとsend_incoming_webhook.phpをサーバ監視ツールへアクセス可能なサーバへ設置し、ログインシェル上から起動すればOK。終了するときはCtrl-Cで。

また、send_incoming_webhook.php単体でも使用可能。

イメージ画像

※この記事をお読みの方はさくらのクラウドやVPS等に設置してください。

外観

前回のものからケースがさらにパワーアップ!着せ替え機能を実装!任意の札をつけることが出来るので「仲良くしてね」とか「個人情報収集中」とか書き放題

平常時(青点灯)

ヤバいとき(赤点滅)

着せ替え札

※さくらのIoTモジュールをポンと置くだけだと「これは何?」と聞かれることが多かったので、「何をしている装置か」をアピールすることは重要だと思います。

今後の課題とか反省点とか

  • secretが設定できないとAPIのURLがバレた時点で悪用されちゃう!何とかして!!(切実)
    ※APIドキュメント側でSecretを設定したパラメータは出力されませんが、設定は可能です。詳しくは【α版のドキュメント】をご参照ください。
  • ステータスについて、2-3日運用してみたものの、赤色になっているときが結構多かった。これじゃあんまり意味無いかも。
    ※くどいようですがサービス全体で見ると稼働率は99.99%以上あります!!(必死)
  • 光り方のパターンはもう少しチューニングしても良いかも。ってかもっと試したい
    →ちなみに現在は対応中になってれば赤色、それ以外は青色になる設定。対応開始は赤にして、対応中は橙色にしてもよさそう。
  • こんなのと組み合わせればふんいき良いかも
    ドーム状アルミフレーム - LED PARADISE☆エルパラ
  • LEDの処理について、赤色の点滅は流れるようなパターンにしたので、テープLEDを延長すればちょっときれいかも

おわり

さいごに

いかがでしたでしょうか。この方法が問題を解決するためのベストなアプローチであるかどうかは別として、思いついたことを目に見える形にするまでの流れは掴んでいただけたかと思います。
(余談ですが、フリーアドレスを実現されたデータセンター事業者がいらっしゃればぜひお話を伺いたいです)

また、今回は諸事情により掲載しているソースをそのまま流用することができず、少しアレンジする必要があるため、前回と比べ敷居はやや高くなるのですが、send_incoming_webhook.phpは改修せずあえて独立したプログラムとしておきました。ある程度プログラミングを経験された方であればcheck.phpに相当する部分をお好きな言語で作成し、通知する内容を「現在の天気」や、「イベント開催時における混雑状況の表示」等に置き換えることも可能かと思います。

ちなみに、本文中にあった円形のブツですが、ある程度の検証を終えた時点で使い道がなくなってしまいました。

そんな折、「さくらのナレッジに公開してみてはいかが?ていうかしろよ?本当はしたいくせに?」とのお声をいただき、「こんな記事でよければぜひ」と思う反面、「えっ?顔出すの?嫌やわ恥ずかしい」という思いもあり少し悩んでいました。

モヤモヤしながらも数日経ったある日、「あ、これならええかな」とあることをひらめき、もう一つ購入しました。(もちろん自腹)

で、その後については私の自画像をご覧いただければ語るまでもありませんが、今思えば逆に目立ってしまったかなぁという気がしなくもありません。

なお、自画像の作成あたって使用した作品(?)は当然、本文中にて触れた使い方ができます。何かのご参考になれば幸いです。

平常時

マズい時

※(2017年3月14日)「動作の流れ」と「使い方」の追記と注釈の一部を修正しました。