当Qiita記事がいいねされたら電飾が光りまくるX'mas Internet of Tree をつくる

【追記 12/3】申し訳程度にツリー確認用のust設置しました → ustre.am/1AxWu

  • ツリー本体は2分間隔の更新にしています(記事中では5分)
  • 配信は遅延があるためいいね後30秒〜2分半でustに反映されます
  • WiFi状況と時間帯と気分によりオフラインなこともあります(適当です)
  • プライバシーのため画角狭めでお許しください

こんにちは、欲しいクリスマスプレゼントはもちろん「いいね」の、dotstudioものづくり担当のうこ(@harmoniko)です。
気付いたらいつのまにか12月ですね!めちゃ寒いですし2017年も終わりそうですし!そんなわけで、IoTLT Advent Calendar 2017 (neo)の1日目を担当させていただきます!遅くなってというかギリギリですいません。。

今年のIoTLTですが実はほとんど登壇しておらず、いいネタが見つかりません。そこでクリスマスネタで誰しもが一度は頭をよぎるInternet of Treeを今回は作ってみたいと思います!
(トップバッターはネタが被らないか気にしなくていいので助かります)

まず素材を調達しよう

12月になる3時間ぐらい前にひらめいたんですが、もうすでに秋月も千石も閉店してしまっておりました。しかし今のご時世、ギャルですらドンキでパーツを買いストリートで半田付けをし自己発光しながらセンター街を歩くというのですから負けていられません。秋葉原のドンキホーテに向かいます。

IMG_20171130_221456.png

クリスマスコーナーを発見しました。ここならツリーとか電飾のLEDとか手に入りそうです!

IMG_20171130_221513.png

反対側にはTENGA特設コーナーがありました。下の階にアダルトエリアあったのになかなか強気の配置です。まあクリスマスとテンガは相性が良いと言われてますのでね(要出典)

IMG_20171130_223041.png

150cmぐらいの大きなタイプにしようと思いましたが、ラスト1で叩き売られていたツリーを救出することにしました。売れ残った上にこんなネタに使われてしまう運の悪さ。

IMG_20171130_221842.png

ビレバンくさい発注ミスの文言があるPOPですがとにかく安くめっちゃ光らせたかったので色違いで3種類、合計300個のLEDを使うことにしました。

IMG_20171130_223228.png

最終的に、クリスマスツリーには欠かせないであろう金色のスターと、夜食用にお菓子の詰まった靴下と、あとはちゃんとツリーが完成しなかったときに備えてインターネットオブテンガで腹を切るべくカップを一つ購入しました。

制作編(ツリー)

必要なもの

IMG_20171130_231234.png

  • Nefry BT
  • お好みのクリスマスツリー
  • おほしさま
  • お好みの色のLEDイルミネーション
  • お菓子入りの靴下
  • お好みのTENGA
  • その他ジャンパワイヤなど

LEDイルミネーションをハックする

IMG_20171130_232352.png

安さのあまり適当に買ってしまったイルミネーションですが、制御方法がわからないので、とりあえず仕組みを理解するところから始めます。どうやらコントローラも内蔵されているようです。 多彩なモードで幻想的に光るとかマジそげぶ

IMG_20171130_232802.png

電池3本を入れてスイッチを押すたびに、8種類の点灯パターンを選んで光らせることができるようです。しかも6時間光ったら自動で消灯し、18時間後に同じパターンで光るという、シンプルで最低限の機能をしっかり押さえています。最初からESP8266とか組み込んで欲しかったなぁ…!

IMG_20171201_014002.png

とりあえず基板をこじあけてみます。外に引き出されているケーブル部分は樹脂できっちりとシーリングされていて、簡易防水性能がありそうです。

IMG_20171201_014242.png

LED100個が繋がったケーブルにかかる電圧を測定してみたところ、3V弱ありました。LEDが点滅するのに合わせてプラスとマイナスを行き来しており、100個のうち半数は順方向電圧で点灯、残り半数は逆方向電圧で点灯するというシンプルな仕組みになっているようです。

Screen Shot 2017-12-01 at 17.34.09.png

おそらく回路としてはこんな感じでしょう。片方を固定にしてもう片方の電位を上下させるか、両方とも上げ下げするかでうまいことやってるようです。さすがに100個のLEDを1個ずつシリアルで制御して500円強とかだったらパーツ屋泣くんじゃないですかね。なんとなくですが、安いものの方がハックしやすく、ある程度以上高価だと逆にAPIが備わっていたりしますね。

IMG_20171201_014457.png

回路全体が電池から吸い上げる電流量は20mAほどです。思ったより少ないですが、これが3つもあるのでNefryから直接駆動は厳しいかもしれません。さすがにドンキでLEDドライバICは見かけなかったので、このまま直での接続を決行します。たぶん各端子の制限電流以上が流せずに暗めで点灯する程度で済むとは思うんですけど、あまりいい方法じゃないので、みなさんは真似しないでくださいね〜〜

IMG_20171201_021647.png

仕組みがわかったところでLEDケーブル部分だけを引っこ抜き、末端をNefryのGPIOに挿せるようにジャンパワイヤ端子を半田付けします。これを3本分作って、Nefryと接続します。

Nefry BTから光らせてみる

IMG_20171201_022802_hdr.jpg

とりあえず3.3VとGNDに接続してみました。思った以上に力強く光ってくれて眩しいです。とはいえNefryが壊れてしまいそうで怖かったのですぐに取り外しました。

IMG_20171201_031958.png

次にLEDケーブルの端子をA0とA1に接続し、電流方向を変えることで点灯の制御ができるかどうか、確認のプログラムを実行してみます。

Nefry-tree-test.ino
#include <Nefry.h>

#define pin1A A0 // IO25 (ESP32)
#define pin1B A1 // IO26 (ESP32)
#define pin2A A2 // IO32 (ESP32)
#define pin2B A3 // IO33 (ESP32)
#define pin3A A4 // IO27 (ESP32)
#define pin3B A5 // IO14 (ESP32)

void led(int id, bool onoff) {
  switch(id) {
    case 1:
      digitalWrite(pin1A, onoff);
      digitalWrite(pin1B, !onoff);
      break;
    case 2:
      digitalWrite(pin2A, onoff);
      digitalWrite(pin2B, !onoff);
      break;
    case 3:
      digitalWrite(pin3A, onoff);
      digitalWrite(pin3B, !onoff);
      break;
    default:
      digitalWrite(pin1A, onoff);
      digitalWrite(pin1B, !onoff);
      break;
  }
}

void setup() {
  pinMode(pin1A, OUTPUT);
  pinMode(pin1B, OUTPUT);
  pinMode(pin2A, OUTPUT);
  pinMode(pin2B, OUTPUT);
  pinMode(pin3A, OUTPUT);
  pinMode(pin3B, OUTPUT);
}

void loop() {
  led(1, true);
  led(2, true);
  led(3, true);
  Nefry.ndelay(500);
  led(1, false);
  led(2, false);
  led(3, false);
  Nefry.ndelay(500);
}

3.3V端子に挿したときより輝度は落ちますが、0.5秒間隔でついたり消えたりを繰り返してくれます。一応、出力を3ペア作ったので、LEDケーブルをA2、A3とA4、A5端子におそるおそる追加してみると……

IMG_20171201_052826.png

なんとか光ってますネ……電圧がかなり落ちてしまうかと思ったのですがそうでもなく、そこそこ明るいです。あとになって過電流で物理的に炎上してファイヤーツリーになってしまわないことを切に願いながらもプログラミング続行です。このLEDケーブルの場合、同時に点灯できるLEDは総数の半分なので、Nefry1つで150個のLEDを駆動していることになります。IoT界隈ってこういう力技が少なくない気がしています。

ツリーにLEDを実装する

IMG_20171130_231738_hdr.png

次はツリーを開封してみます。安かったわりにこれもしっかりしてて、このまま弊社の入り口に据え置きたいぐらいです。スミマセンスミマセンとつぶやきながらLEDイルミネーションのケーブルを巻きつけてゆきます。巻きながら気付いたのですがこのLEDのケーブル、1本が10mほどあって、3分の1巻きつけたところでツリー全体を覆ってしまいました。でもせっかく買ったので3種類とも全部巻いてしまいます。今回の制作で一番疲れました。

IMG_20171201_064212.png

パッと見ただけではよくわかりませんが、ケーブル合計30m・300個のLEDが詰め込まれたツリーになりました。ハードウェア部分はこれで一旦終了です。

制作編(Nefry BT)

プログラム要件

  • WiFiに接続
    • このあたりは全部Nefry BTがやってくれます。とてもべんり。
  • QiitaのAPIを使用してこの記事についた最新いいねの時刻を定期的に取得し、変化があれば「いいね」されたと判断する
    • いいねAPIは記事本体のメタデータを取得する方法といいねしてくれたユーザーのリストを取得する方法がありますが、前者だと一気に記事全体を受信してしまうため、後者の先頭部分(常に更新日時降順で返却されます)だけを読み込むことでメモリとレスポンス時間を節約します。
    • APIのアクセス頻度は、記事に関するGETなので認証不要のものとなり、1時間あたり60回まで許可されます。でも毎分更新する必要もないので、5分おきに更新確認するものとします。
  • 新規いいねを検出したらLEDをめいっぱい点滅させまくる
    • 次回更新まで5分ぐらいビカビカやっちゃえばいいとおもいます。

記事に関してですが、一度投稿しないとIDがわからないので、テスト中は弊社代表のびすけのこの記事をずっといいねしたり解除したりしてました。いい記事なのでぜひ見てください。。。

コード

中身に関してはコードの中に直接コメントアウトで説明を記したのでまずは見てくださいませ。

Nefry-Xmas-tree.ino
#include <Nefry.h>
#include <WiFiClientSecure.h>

#define reload 300000UL // 更新間隔(ミリ秒)5分設定にしています
#define pin1A A0 // IO25 (ESP32)
#define pin1B A1 // IO26 (ESP32)
#define pin2A A2 // IO32 (ESP32)
#define pin2B A3 // IO33 (ESP32)
#define pin3A A4 // IO27 (ESP32)
#define pin3B A5 // IO14 (ESP32)

WiFiClientSecure client;
bool LEDState;
unsigned long next;
const char* host = "qiita.com";   // ↓ここに記事のIDを入力する
const char* page = "/api/v2/items/566899520520f442ef4f/likes";
const char* root_ca= \
    "-----BEGIN CERTIFICATE-----\n" \
    "ルート証明書は\n" \
    "ここにおいてください\n" \
    "改行などもうまく合わせるように\n" \
    "なっがいので以下略\n" \
    "-----END CERTIFICATE-----\n";


void led(int id, bool onoff) {
  // GPIO間で電流の方向を切り替えます
  switch(id) {
    case 1:
      digitalWrite(pin1A, onoff);
      digitalWrite(pin1B, !onoff);
      break;
    case 2:
      digitalWrite(pin2A, onoff);
      digitalWrite(pin2B, !onoff);
      break;
    case 3:
      digitalWrite(pin3A, onoff);
      digitalWrite(pin3B, !onoff);
      break;
    default:
      digitalWrite(pin1A, onoff);
      digitalWrite(pin1B, !onoff);
      break;
  }
}
void ledOff() {
  // 全消灯
  digitalWrite(pin1A, LOW);
  digitalWrite(pin1B, LOW);
  digitalWrite(pin2A, LOW);
  digitalWrite(pin2B, LOW);
  digitalWrite(pin3A, LOW);
  digitalWrite(pin3B, LOW);
}


bool checkLike(bool detail) {
  // 最新のいいねされた時刻の文字列表現
  static String newestLike = "";
  String likeNow = "";
  // 接続します
  Nefry.print("Connecting...");
  client.setCACert(root_ca);
  if (!client.connect(host, 443)) {
    // 接続失敗
    Nefry.println(" failure.");
    client.stop();
    return false;
  }

  // 接続成功
  Nefry.println(" success.");
  // GET送信
  client.print(String("GET ") + page + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");
  Nefry.println("https://" + String(host) + String(page) + "\n");
  // タイムアウト待ち(10秒)
  int timeout = millis() + 10000;
  while (!client.available()) {
    if (millis() > timeout) {
      Nefry.println("Connection timed out.");
      client.stop();
      return false;
    }
  }
  // データ受信
  while (client.available()) {
    String line = client.readStringUntil('\r');
    if (line.length() == 1) {
      Nefry.println("\n");
      // レスポンスボディ
      // JSONボディの最初のコロンとカンマのあいだに日付時刻文字列があります(力技)
      line = client.readStringUntil(':');
      likeNow = client.readStringUntil(',');
      client.stop();
      break;
    } else {
      // レスポンスヘッダ
      if (detail) Nefry.print(line);
    }
  }

  // 最新いいねの時刻を表示
  Nefry.print("Liked at "); Nefry.println(likeNow);
  if (newestLike.equals(likeNow)) {
    // 以前の日付時刻と同じだったとき
    return false;
  } else {
    // 以前の日付時刻と異なるとき・新規いいね獲得!
    newestLike = likeNow;
    Nefry.println("New 'iine' earned !");
    return true;
  }
}


void flashLEDLoop(bool onoff) {
  static bool state = true;
  if (onoff) {
    // 点滅
    led(1, state);
    led(2, state);
    led(3, state);
    state = !state;
  } else {
    // 消灯
    ledOff();
  }
}


void setup() {
  Nefry.setLed(0, 0, 0);
  pinMode(pin1A, OUTPUT);
  pinMode(pin1B, OUTPUT);
  pinMode(pin2A, OUTPUT);
  pinMode(pin2B, OUTPUT);
  pinMode(pin3A, OUTPUT);
  pinMode(pin3B, OUTPUT);
  // 起動直後時点の最新いいね時刻を取得(接続エラーでなければ必ずtrueが返ります)
  Nefry.print("First load: ");
  LEDState = checkLike(true);
  next = millis() + reload;
}


void loop() {
  // reloadミリ秒おきにチェックします
  // 新しくいいねされていたら次のreloadミリ秒間はずっと光ってます
  if (millis() > next) {
    next = millis() + reload;
    LEDState = checkLike(false);
  }
  flashLEDLoop(LEDState);
  Nefry.ndelay(200);
}

あらかじめWiFiのセットアップを済ませたNefry BTを用意し、以上のプログラムを書き込んでシリアルモニタから動作を確認します。

ポイント1:HTTPS接続

Qiita APIのドキュメントにも書いてありますが、HTTPS通信が必須となっています。最初よく読んでなくて、HTTPSを使わないWifiClientクラスで通信してたら以下のレスポンスが返ってきました。

GET /api/v2/items/ceaa09ef8898bee8369d HTTP1.1
Host: qiita.com

HTTP/1.1 301 Moved Permanently
Content-Type: text/html
Date: Thu, 30 Nov 2017 23:50:01 GMT
Location: https://qiita.com/api/v2/items/ceaa09ef8898bee8369d
Server: nginx
X-Request-Id: ******
Content-Length: 0
Connection: Close

はい出ました301。REST API使ったりしない限りは自分でハンドリングすることもないので、たまに見ると面倒になります。最初からHTTPSで接続せよってことですが、ラッキーなことにWifiClientSecureクラスがポート443での接続を助けてくれます。ただし、ルート証明書をNefryに入れておかないといけないのでどのみち面倒さはありますが、組み込み端末ですらセキュア化からは逃れられない時代ですのでやっちゃいましょう。

Screen_Shot_2017-12-01_at_8_53_50.png

OS XのChromeでの説明になりますが、まずqiita.comのページ内でデベロッパーツールを開き、セキュリティタブの中にある「証明書を表示」みたいなボタンを押してSSL証明書を確認します。そしたら上部にポップアップが出てきますが、ルート証明書となるのはリストの一番上です。 Starfield Services Root Certificate Authority - G2 っていうのがありますね。こいつがルパンだ。

Screen_Shot_2017-12-01_at_9_00_46.png

OS Xの場合、Keychain Accessのシステムルートの項目を開くと証明書がたくさん入ってますので、さっきの証明書と同じ名前のものを探してデスクトップなどにエクスポートしちゃいましょう。.cerファイルが出てきますが、このバイナリをそのままNefryに書き込むのは大変なので、Base64形式にエンコードしてプログラム定数として埋め込んでしまいます。

Screen Shot 2017-12-01 at 9.07.16.png

例えばですが、base64encode.uic.jpのようなオンラインでエンコードをしてくれるようなサイトがあるので、証明書ファイルを指定して表示ボタンを押すと一瞬で文字列を表示してくれます。\nのような改行コードを含んでますので、形式を上手に合わせて変数に格納してやりましょう。この場合エンコードしたあとの証明書をそのままコピーしてしまってよいのかわからなかったので、この記事では省略しています。

ポイント2:JSONパーサ

Qiitaに限らずですが、JSONで値を返却してくれるAPIはかなり多いです。で、リッチな言語で扱うなら全く問題にならないんですが、Arduinoとかそのあたりの組み込み系で扱うのはかなり面倒なんですよね。ライブラリはあるっぽいんですが、ほとんどの場合でメモリと時間の無駄だと思ってたりします。

で、今回の例で言うと

Screen_Shot_2017-12-01_at_22_16_24.png

この赤い枠の中の日付が欲しいだけなので、 いちいちパースせずにreadStringUntilメソッドを2回使って「最初のコロンまで取得→捨てる」してから「最初のカンマまで取得」するとバッファをほとんど読まなくていいんでないかなと思います。文字数が変わらないんだったらインデックス決め打ちでsubstrとかやっちゃってもいいかもですね。

* Stringクラスと間違えてたため修正しました。

たぶんできたと思うので試してみよう

はい、同じくこの記事をターゲットにしてテストしてみました。ダンプ出力はシリアルモニタから見ることができます。(この記事の投稿後は、NefryBT側のIDをこの記事のものに書き換えておいてます)

Connecting... success.
https://qiita.com/api/v2/items/ceaa09ef8898bee8369d/likes

HTTP/1.1 200 OK
(略)
Connection: Close

(起動直後のアクセス)
Liked at "2017-11-30T22:09:34+09:00"
New 'iine' earned !(最初は必ず光ります)
Connecting... success.
https://qiita.com/api/v2/items/ceaa09ef8898bee8369d/likes

(5分後)
Liked at "2017-11-30T22:09:34+09:00"
Connecting... success.
https://qiita.com/api/v2/items/ceaa09ef8898bee8369d/likes

(ここで僕がいいねを押す)

Liked at "2017-12-01T10:41:41+09:00"
New 'iine' earned !(光りました!!!)

IMG_20171201_072113_hdr copy.png

5分おきの更新なので、いいねを押してすぐに光るわけではありませんが、少し待ってればちゃーんと光ってくれました!
全体的にパワーで乗り切った感があるので、今度からちゃんとLEDドライバ使ってやりたいと思います……

反省(まとめ)

みんな大好きQiitaでIoTができるのは楽しいな!って思ったんですけど、記事を見てくださってるみなさんはいいね押すことはできてもフィードバックがない(ツリーが動作してるとこを見れない)ので、もったいなさと申し訳なさを感じております。
本当はアドベントカレンダーが公開されたらその日の深夜まで光るという仕様にしたかったんですけど、APIはなくHTMLパースも大変そうだったので諦めましたorz
あと疲労のせいかずっと深夜のテンションで記事を書いてます、ごめんなさい。。。12月はみなさん忙しいでしょうけど頑張って乗り切っていきましょう!
それでは、たくさんのいいね!おまちしております。
5分後にもう一度いいねボタン押して解除のほどもお忘れなく!

ところで話は変わりますが、 年内最後のIoTLTが12月26日に渋谷のレバレジーズで開催されます!!っででで、もし可能だったらですがこのツリーを持っていこうかなあと思いますので、みなさんよろしければ忘年会がてら遊びにきてくださいね!!
【増席】IoT縛りの勉強会! IoTLT vol.34 @ teratail (レバレジーズ)

おまけ

IMG_20171201_072113_hdr_copy.png

勢いだけでツリーに飾り付けられてしまったかわいそうなお菓子の靴下とテンガですが、上記の今月26日のIoTLTで希望者にプレゼント(多数の場合はじゃんけん)したいと思いますので、重ねてご来場お待ちしております〜〜
それではよい師走を!