144Labの入江田です。
うんこボタンについて
https://unkobtn.com にて育児記録の支援ツールとして販売・運用中です。
「うんこボタン」デバイス自体は単純な2つのボタンとWi-Fi機能を持つIoTボタンデバイスです。
ガジェット好きの方々に自由に改変してもらって使ってもらいたいという狙いが当初からありまして、マクアケ ではハッカーコースなんてものもありました。
ここで改めて、「うんこボタン」のハック方法を解説します。
うんこボタンの仕組み
- 押しボタンと赤LED、緑LEDのセットを左右に持ちます。
- Wi-Fi機能を有するマイコン=ESP8266を持ちます。
- またリセット制御ブロックを周辺に持ちます。
- あと、マイコンを書き換えるモード指定(JP2)とデバッグ用のシリアルポート(JP1)も持っています。
回路図
ハックに別途必要なもの
※注: 上記のUSBシリアル変換アダプタから供給可能な電力では能力が不足しますので別途電池か電源をご用意ください
Arduino IDE のセットアップ
STEP1
Arduino IDE version 1.8.7 をお使いのPCにインストール
STEP2
Arduino IDE の設定画面にて「追加のボードマネージャのURL :」のところに 「 http://arduino.esp8266.com/stable/package_esp8266com_index.json 」と入力してください。
注意:すでに何か設定済みである場合は、「,」カンマ区切りで複数記述するか右端のボタンをクリックして追加してください。
STEP3
ボードマネージャを開きます。 (メニューの「ツール」「ボード: ???」「ボードマネージャ...」をクリック)
検索のところで「esp8266」とか入力すると該当ボードが出てきます。 「esp8266 by ESP8266 Community」 にてバージョン2.4.2を選んでインストールしてください。(ここでのダウンロードは15MBほどあります)
ライブラリマネージャを開きます。 (メニューの「スケッチ」「ライブラリをインクルード」「ライブラリを管理...」をクリック)
ライブラリマネージャから 「ArduinoJson 5.13.3」にてバージョン5.13.3を選んでインストールしてください。
注:バージョンは動作確認時のものです。最新安定版をご利用ください。
STEP4
ArduinoIDEの設定を行います。
ボード用設定
- CpuFrequency=160
- FlashFreq=80
- FlashMode=qio
- UploadSpeed=230400
- FlashSize=2M
- ResetMethod=none
- シリアルポートはPC環境ごとに適切に選択
シリアルモニタ設定 * ボーレート 74800(推奨値)
ファーム書き換え用の結線
- 「JP1」と「FTDI USBシリアル変換アダプター Rev.2」を以下の配線でつなぐ
- JP1:GND-アダプタ:GND
- JP1:TXD-アダプタ:RXD
- JP1:RXD-アダプタ:TXD
- JP1:VIN-3.3V電源(電池を繋いでる場合は不要)
実際の接続の様子
JP1のTXD、RXD、GNDにヘッダピンを立てます。
JP2にもピンヘッダを立てます。
ピンヘッダを立てる際、ケースに当たらないよう基板面から8mm高さ以下になるようにしてください(長い場合ピン先端をカットしてください)
USBシリアル変換アダプタをつなぎます。
バックアップ(復旧が必要な場合)
うんこボタンのファームウェアは個体識別用の情報も含むためWebで提供しておりません。うんこボタンとして復旧したい場合はesptool.py等でFlashメモリ全体のフルバックアップを取っておきます。
あらかじめesptool.pyを使えるようにしてください。 例: pip3 install esptool
- JP2をショートした状態で電源を通電(電池をちょっと外して戻す)
esptool.py -p <シリアルポート> chip_id <chip_id> esptool.py -p <シリアルポート> -b 460800 read_flash 0 0x200000 backup.bin
ここで保存される「backup.bin」は<chip_id>を持つデバイス専用のファームウェアなので複数のデバイスを取り扱うときは取り違えないようbackup-<chip_id>.binなどにリネームしておくと良いでしょう。
リストア
あらかじめバックアップしたファイル(例: backup-<chip_id>.bin )を用意します。
- JP2をショートした状態で電源を通電(電池をちょっと外して戻す)
esptool.py -p <シリアルポート> chip_id <chip_id> esptool.py --port <シリアルポート> write_flash 0x0 backup-<chip_id>.bin
自作コードの書き込み
- JP2をショートした状態で電源を通電(電池をちょっと外して戻す)
後述のサンプルのようなコードを用意してArduinoIDEからマイコンへの書き込みを実行する
自作コードの動作確認
- JP2をオープンにした状態でボタンどちらかを押す
シンプルなサンプル
#include <ArduinoJson.h> #include <ESP8266HTTPClient.h> #include <ESP8266WiFi.h> #include <ESP8266WiFiMulti.h> // IOピン宣言 #define LEFT_BTN 14 #define RIGHT_BTN 12 #define ACTIVE 16 #define RBOOTED 13 #define LBOOTED 5 #define LEFT_RED_LED 15 #define RIGHT_RED_LED 4 #define LEFT_GREEN_LED 2 #define RIGHT_GREEN_LED 0 bool lBooted; bool rBooted; int batteryLevel; HTTPClient http; ESP8266WiFiMulti wifiMulti; // 定形初期化処理(理解するまでは変更しないで) void boot() { Serial.begin(74800); // pin setup pinMode(LBOOTED, INPUT_PULLUP); pinMode(RBOOTED, INPUT_PULLUP); pinMode(LEFT_RED_LED, OUTPUT); pinMode(RIGHT_RED_LED, OUTPUT); pinMode(LEFT_BTN, INPUT); pinMode(RIGHT_BTN, INPUT); // スリープから復帰するのに押されてたボタン状態を取得 lBooted = digitalRead(LBOOTED) == LOW; rBooted = digitalRead(RBOOTED) == LOW; // アクティブモードへ移行 digitalWrite(ACTIVE, LOW); pinMode(ACTIVE, OUTPUT); // ACTIVE==LOWのあとでしか緑LEDを使えない pinMode(LEFT_GREEN_LED, OUTPUT); pinMode(RIGHT_GREEN_LED, OUTPUT); // バッテリーレベル取得(アクティブモードでないとダメ) batteryLevel = analogRead(A0) * 3000 / 1024; // [mV] // 電池が消耗していたり、通電のみだった場合はすぐにスリープする if (batteryLevel < 2300 || !lBooted && !rBooted) { ESP.deepSleep(0); delay(500); } } void setup() { boot(); // 必ずsetup関数の最初に実行 Serial.println(); Serial.println("left: " + String(lBooted) + " right: " + String(rBooted)); if (lBooted) { digitalWrite(LEFT_GREEN_LED, HIGH); // left green on } if (rBooted) { digitalWrite(RIGHT_GREEN_LED, HIGH); // right green on } delay(1000); ESP.deepSleep(0); delay(500); } void loop() { }
押したボタンに応じたメッセージをSlackにポストするサンプル
#include <ArduinoJson.h> #include <ESP8266HTTPClient.h> #include <ESP8266WiFi.h> #include <ESP8266WiFiMulti.h> // Wi-Fi接続パラメータ const char* ssid = "********"; const char* password = "********"; // SlackのIncoming WebHooksにて取得したURLをココに記述します。 const char* uri = "https://hooks.slack.com/services/#######/#######"; // ブラウザでHTTPSサイト(上記URL)のサーバ証明書を表示して // SHA1 fingerprint(指紋)をここに貼り付けること。 const char* fingerprint = "C1 0D 53 49 D2 3E E5 2B A2 61 D5 9E 6F 99 0D 3D FD 8B B2 B3"; // IOピン宣言 #define LEFT_BTN 14 #define RIGHT_BTN 12 #define ACTIVE 16 #define RBOOTED 13 #define LBOOTED 5 #define LEFT_RED_LED 15 #define RIGHT_RED_LED 4 #define LEFT_GREEN_LED 2 #define RIGHT_GREEN_LED 0 bool lBooted; bool rBooted; int batteryLevel; HTTPClient http; ESP8266WiFiMulti wifiMulti; DynamicJsonBuffer jsonBuffer; // 定形初期化処理(理解するまでは変更しないで) void boot() { Serial.begin(74800); // pin setup pinMode(LBOOTED, INPUT_PULLUP); pinMode(RBOOTED, INPUT_PULLUP); pinMode(LEFT_RED_LED, OUTPUT); pinMode(RIGHT_RED_LED, OUTPUT); pinMode(LEFT_BTN, INPUT); pinMode(RIGHT_BTN, INPUT); // スリープから復帰するのに押されてたボタン状態を取得 lBooted = digitalRead(LBOOTED) == LOW; rBooted = digitalRead(RBOOTED) == LOW; // アクティブモードへ移行 digitalWrite(ACTIVE, LOW); pinMode(ACTIVE, OUTPUT); // ACTIVE==LOWのあとでしか緑LEDを使えない pinMode(LEFT_GREEN_LED, OUTPUT); pinMode(RIGHT_GREEN_LED, OUTPUT); // バッテリーレベル取得(アクティブモードでないとダメ) batteryLevel = analogRead(A0) * 3000 / 1024; // [mV] // 電池が消耗していたり、通電のみだった場合はすぐにスリープする if (batteryLevel < 2300 || !lBooted && !rBooted) { ESP.deepSleep(0); delay(500); } } void setup() { boot(); // 必ずsetup関数の最初に実行 if (lBooted) { digitalWrite(LEFT_GREEN_LED, HIGH); // left green on } if (rBooted) { digitalWrite(RIGHT_GREEN_LED, HIGH); // right green on } Serial.println(); Serial.println("left: " + String(lBooted) + " right: " + String(rBooted)); // Wi-Fi connect WiFi.mode(WIFI_STA); wifiMulti.addAP(ssid, password); } void loop() { if (wifiMulti.run() != WL_CONNECTED) { delay(500); return; } String message; if (lBooted && !rBooted) { message = "左を押した"; } else if (!lBooted && rBooted) { message = "右を押した"; } else if (lBooted && rBooted) { message = "両方を押した"; } if (message.length() == 0) { return; } // TLS通信においてルート証明書チェインの検証機能をESPは持っていない。 // サーバー証明書のsha-1指紋(fingerprint)が合致するか簡易の検証を行う。 // なのでsha-1指紋を予めコードに埋めておく必要がある。 Serial.println(uri); JsonObject& json = jsonBuffer.createObject(); json["username"] = String("button[") + String(ESP.getChipId()) + String("]"); json["text"] = message; json["icon_emoji"] = ":ghost:"; String buff; if (!json.printTo(buff)) { Serial.println("json encode failed"); return; } http.begin(uri, fingerprint); int code = http.POST(buff); String content = http.getString(); http.end(); Serial.println(String(code)); Serial.println(content); // parse json JsonObject& resp = jsonBuffer.parseObject(content); // タスクが終了したら必ずディープスリープすること。 ESP.deepSleep(0); delay(500); }
定形処理内容の解説
ディープスリープからのウェイクアップはリセットブートのことです。 lBootedとrBootedはウェイクアップ要因が左ボタンか右ボタンかを示すフラグです。 ブートしたら上記フラグを読みだしたあと必ずACTIVEにLOWを出力すること(回路の仕様です)。 両方押された場合は双方trueになります。 双方falseの場合はなにもタスクを実行することなくスリープするのが推奨です。 バッテリー電圧チェックはACTIVE==LOWである必要があります。 緑LEDを灯すためにはACTIVE==LOWである必要があります。
上記のような制約と手順をまとめた関数がboot()関数です。
その他の注意点
- うんこボタンのファームウェアは個体識別用の情報も含むためWebで提供しておりません。復旧したい場合はesptool.py等でFlashメモリ全体のフルバックアップを取っておいてください。
- HTTPS通信ではメモリを大量に使うので送受信に数百キロバイトのコンテンツを扱おうとすると暴走(メモリオーバーフロー)する場合があります。
まとめ
本デバイスをハックすることでIoTの世界に踏みこんでみてください。 ボタンを押したら何が起こるのかはアイディア次第です。
では、よいハックを。