この記事はおうちハックAdvent Calendarの 14日目の記事です。昨日の記事は @kei_s さんの「床暖房を押したいけど、押せてない話」です。Microbot Pushめっちゃ興味あったのですが、現時点ではなかなか難しい部分もあるようです。いわゆるIoT機器は情報表示能力が貧弱なケースが多く、何が起こってるのかよくわからないんですよね。。
今日もそんな比較的貧弱なデバイスの話です。妻のために、ボタンを押すと今の天気を光の色で教えてくれるキーホルダーを作った際に気がついた諸々をまとめています。サーバが持つ情報に従い、スマホ経由でBLE搭載ハードウェアを制御するという少し前に流行った構成をプロトタイプしてみたという記録にもなっています。
作ったもの
- キーホルダーです。基板が埋め込まれています。
- キーホルダーに埋め込まれているボタンを押すと、LEDが光ります。
- LEDの光の色は現在地の天気によって変わります。現在地で雨が降っている場合青色に光ります。雨が降っていない場合は赤く光ります。
- これをつかうことでわざわざ窓の外を確認したり、PCやスマホの天気情報を見なくても、ボタン一つで傘を持って出かけるべきか否かがわかります。
- 現在地情報はスマホのGPSから取得しています。
- 天気情報はウェブから取得しています。
- 回路図・部品表・ソースコードはgithubにあります。
経緯
- 「おうちハック業界」ではハックを行うにあたり、家族、特に妻の許可をいかにして得るかが重要かつ難易度の高い課題とされています(通称妻ハックと言います。)。名著「Raspberry Piではじめるおうちハック」にもおうちハックを行うにあたり妻から以下に許可をもらうか、さまざまなアイデアが記載されています。
- 僕はこれまで「細かい温度制御をして美味しいパンが焼けるトースター」など、いくつかのおうちハック機器をプロトタイプしてきたのですが「妻が自分で使うレベル」まで持っていくことができていませんでした。使用方法が複雑だったり、基板がむき出しだったりするのが原因だと思います。
- そこで今回は妻をユーザとし、妻が欲しいものを作ることで、堂々とものづくりをしつつ家庭にも貢献するという方針のもと、システムを作ってみることにしました。
- 我が家(マンションの4階)では冬はカーテンを締めっぱなしにしたまま朝出かける事が多いのですが(寒いので)、その際「階段をおりて1階にたどり着いたあとで雨が降っていることに気づき、家まで戻ってくる」というケースが多くあります。
- 妻の職場でも似たようなことがあるとのこと。
- 窓の外を見たり「東京アメッシュ」のようなサイトを見たり、折り畳み傘を常備しておけばいいだけの話なのですが、忙しい朝にそこまで気が回らないのです。
- この課題を解決するために「ボタンを押すだけで現在地の天気を知ることのできるキーホルダー」を作ってみることにしました。
全体の方針と構成
- せっかくなので勉強も兼ねて、つくれる部分は自分でつくるという方針にしました。
- MESHやlinking、Amazon EchoやmyThingsを使えば、同等以上の機能を持ったものがもっと気軽にかつセキュアに作れます。
- ただ今回は浅く広く色んなところを触ってみたいと思い、回路・ファーム・アプリ・サーバサイドをそれぞれ自分で書いて見ることにしました。まあ、趣味なので。
こんな感じの構成になっています。ユーザはせいぜい二人なので、スケール等はあんまり考えていません。
- ユーザがボタンを押す
- デバイスはBLEのアドバタイズを開始。スマホ側でそれを受ける
- スマホは現在の位置情報をGPSで取得しAWSに送信
- AWSは外部サービスから位置を元にした天気情報を取得し、スマホに送信
- スマホは天気情報に対応する色情報をBLEデバイスに送信
- デバイスが光る
回路をつくる
まずは回路を作ります。環境は以下のような感じです。
- CAD: Eagle6.5 (無償版)
- 基板製造サービス: elecrow
- 部品実装: オーブントースター
- 調達: digikeyとマクニカオンラインストア
回路はこんな感じになりました。
Bluetoothモジュールとして、マクニカ社から販売されているkoshianを使用します。その周りに電源(ボタン電池)・フルカラーLED・タクトスイッチをつけただけのシンプルな構成です。ちゃんと製品にするなら電池がなくなってきたときにLEDの光が弱くならないように・電池が逆に刺されたときのことを考えて・電波が十分飛ぶように部品を置く位置を考慮して、など諸々検討すべき項目があると思うのですが、今回はスキップしてしまいました。
これをパターンに起こして、elecrow社に8.9USD払うと、1週間くらいで基板が届きます(すごい!)。今回は基板にくわえて部品のはんだ付けに便利なステンシルもつけてもらいました。それとは別に電子部品を通販で買っておきます。
基板とステンシルです。ステンシルは、クリームはんだを基板に塗るために使います。こちらの記事が非常に参考になりました。
クリームはんだを塗って、オーブントースターで加熱します。去年作った温度制御ができるオーブントースターが役に立ちました。
すると、はんだ付けが完了します。ハンダゴテいらず。楽です。
デバイスのファームウェアをつくる
今回使用したkoshianというモジュールには、Cypress社のCYW20737SというSoC(System on Chip)が搭載されています。Bluetoothや各種IOに関するCのAPI及び開発環境が提供されているので、自分でファームを開発することができます。
実現したい機能は以下のとおりです。
- ボタンが押されたことを検知して、Bluetoothの通信を始める。
- Bluetoothでスマホに接続し、スマホから天気の情報を取得する。
- 取得した天気情報を元にLEDを光らせる。
- 不要なときには電波を飛ばさずスリープ状態になる。
ファームウェアのプログラムに関しては、どのSoC/SDKを使用するかで書き方が全く異なります(もちろん、回路によっても異なります)。今回はCypressのSoCを使いましたが、別の会社のものを使う場合は以下のサンプルコードは役に立ちません。が、一応貼っておきます。雰囲気こんな感じです。
ボタンが押されたことを検知して、Bluetoothの通信を始める。
// ボタンが押されたときに呼び出される割り込みハンドラ
// デバイス初期化時に予め登録しておく
static void ame_gpio_interrupt_handler(void *parameter, UINT8 arg) {
UINT8 pin = (UINT8)(UINT32)parameter;
gpio_clearPinInterruptStatus(PIN_SWITCH >> 0x04, PIN_SWITCH & 0x0f);
UINT8 input;
if (gpio_getPinInput(PIN_SWITCH >> 0x04, PIN_SWITCH & 0x0f)) {
input |= 0x01 << pin;
} else {
input &= ~(0x01 << pin);
}
if (input == 0) {
// ボタンが押されたタイミングでBLEのアドバタイズを開始する
bleprofile_Discoverable(HIGH_UNDIRECTED_DISCOVERABLE, NULL);
}
}
スマホからデータを書き込む
// スマホとデバイスがconnectされ、スマホからcharacteristicに書き込みが
// あった際に呼ばれるハンドラ。
int ame_write_handler(LEGATTDB_ENTRY_HDR *p) {
UINT16 handle = legattdb_getHandle(p);
int len = legattdb_getAttrValueLen(p);
UINT8 *attrPtr = legattdb_getAttrValue(p);
UINT8 color = attrPtr[0];
switch (color) {
case BLE_COMMAND_R:
// コマンドの中身(今回は単純にunsigned int 1byteにしました)が
// 「赤」ならLEDを赤く点滅させる。
led_start(LED_PWM_R);
break;
case BLE_COMMAND_B:
// 「青」ならLEDを青く点滅させる。
led_start(LED_PWM_B);
break;
}
return 0;
}
iOSアプリをつくる
iOSアプリがやることは
- GPS情報から現在地を取得する
- BLEデバイスからのアドバタイズを受取る
- サーバから天気情報を取得する
- バックグラウンドで動かす
あたりです。
実は少々時間が足りず、バックグラウンドで動かすところまでたどり着けませんでしたorz。それ以外の部分はAlamofire, CoreLocation, CoreBluetoothのサンプルをそのまま動かしているような感じです。以下はBluetooth部分のコードですが、connect
->serviceの取得
->characteristicの取得
->characteristicへの書き込み
->disconnect
という定番の流れにそっています。
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
peripheral.delegate = self
peripheral.discoverServices([AME_SERVICE])
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else{
print("error to find service")
return
}
for service in services {
if (service.uuid.isEqual(AME_SERVICE)) {
peripheral.discoverCharacteristics([AME_WEATHER_C12S], for: service)
}
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard let c12ss = service.characteristics else {
print("error to finc c12s")
return
}
for c12s in c12ss {
if (c12s.uuid.isEqual(AME_WEATHER_C12S)) {
var value: CUnsignedInt = CUnsignedInt(self.weather)
let data = NSData(bytes: &value, length: 1)
peripheral.writeValue(data as Data, for: c12s, type: CBCharacteristicWriteType.withResponse)
}
}
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
self.centralManager.cancelPeripheralConnection(peripheral)
}
iOSとBLEデバイスのファームウェアに関しては、以下のようなところを今後改善していきたいと考えています。
- 現状だと誰でもデバイスに自由につなげてしまうので、初期設定としてスマホとデバイスをペアリングすべき。
- 通信の内容が生で見えてしまうので、アプリケーションレイヤでの暗号化を入れるべき? (天気データなので別にいいか?)
- レスポンスの向上(遅い!)
- 低消費電力化 (現状電力計測もしていない。データを取って必要なところまで下げたい)
サーバサイドをつくる
AWS lambda上のnode.jsで書きました。lambdaは東京アメッシュの画像データと、スマホから送られてきたGPS情報を元に、現在地点で雨が降っているか否かを判定します。(一応、東京都水道局にはこのようなデータの使用を行っていることを連絡済みです)。
東京アメッシュのサイトは複数の画像(県境を示す画像、地形を示す画像、降雨量を示す画像)を重ね合わせて表示しており、そのうち降雨量を示す画像が時間によって更新されるという仕様になっています。これを取得します。
getMapUrl(now) {
const Y = now.getFullYear();
const M = now.getMonth() + 1;
const D = now.getDate();
const h = now.getHours();
const _h = h < 9 ? "0" + h : h;
const m = now.getMinutes();
let _m = Math.floor(m / 5) * 5;
_m = (_m % 5 === 0) ? _m - 5 : _m;
_m = (_m < 9) ? "0"+_m : _m;
return 'http://tokyo-ame.jwa.or.jp/mesh/000/' + Y + M + D + _h + _m + '.gif';
}
取得した画像のうち、現在地のみをcropしてその領域に雨がないかどうかを判定します。画像処理にはimagemagick-stream
を使用しました。
getWeather(pos, callback) {
// 東京アメッシュからデータを取得する
const path = _this.getMapUrl(new Date());
const read = request.get(path);
const x = pos.x - 10;
const y = pos.y - 10;
const croppos = "20x20+" + x + "+" + y;
// 取得した画像の現在地だけを切り出し、bmpに変換するためのstream
const convert = im().set('crop', croppos).outputFormat('bmp');
let conv = read.pipe(convert);
conv.on('data', (data) => {
const buf = Buffer(data);
let sum = 0;
// 取得されたbufferの10byte目から画像データが始まる
// 雨が降っていない領域の画像は0xffffffになるため、
// bufferを1byteずつ読み、合計が0でなければ雨が降っているとみなす。
// もっといいやり方ないですかね...?!
for (var i = buf[10]; i < buf.length; i++) {
sum += buf[i];
}
let weather = sum === 0 ? "sunny" : "rainy";
callback(null, weather);
});
}
外装をつくる
- とりあえず、今回は露店で売ってた謎のクリスマス的人形に入れてみています。が、かわいさが足りない。。
- 基板をぬいぐるみ的なキーホルダーに入れようとおもっていますが、まだかわいいのが見つかっていません。誰か教えてください。
感想
そんなこんなで、一応動く物ができました。いろいろ思うことがあったので感想を書いてみます。
ここまでは誰でもできる
- 基板は10ドル以下でつくれ、BLEモジュールも980円で手に入り、その他の部品も1プロダクトあたり全部で100円以下くらいだと思います。ハードウェアは大量生産が基本ですが、妥協できるところを妥協すればBLEデバイスくらいなら原価1200円くらいでサクッと作れる模様です(もちろん、筐体を金型で作るとかってなればその限りではありません)。
- ソフトウェアに関しても、正直サンプルコードを組み合わせるだけで作れました。Qiitaさまさまです!
本当はここからが大変
- ただ、本来はここからが大変なのだろうと思います。もし、量産したいのならば。
- MVPをつくれたとしても、現状保留しているセキュリティの担保、ソフトウェア/ハードウェアのつくりこみ、UIデザイン、通信/サーバ/ハードウェアの信頼性の担保、量産工程の設計と信頼性試験の実施、各種法規やステークホルダーとの調整、そもそもお金を出して作る価値があるのか問題などなど、検討すべき課題はたくさんあるなあと思いました。
でも、これはおうちハックなので
- とはいえ、別に量産したいわけではなく、自分の家庭を少し便利で楽しいものにしたいだけなので、これはこれでよいのかなと思います。ほとんどの場合、おうちを便利にするにあたり基板をおこしたりソフトウェアを書いたりする必要はないと思うのですが、可能性を広げるという意味で色々できると思いつく幅もひろがるかなーとかとか。
目の前にターゲットユーザ
- 僕は「◯◯がほしすぎて作る起業家タイプ」ではないので、目の前にターゲットユーザがいることが非常に大事です。
- 今回のターゲットユーザは妻だったのですが、これはベストですね。
- 出来上がりに対し忌憚なき意見をくれるorz
- ただユーザというだけでなくプランナ的に振る舞ってくれる(自分が欲しいものを言ってくれる)
- なかなか普段忙しいと、ユーザのことを考えた開発っていうのができなくなっちゃいがちなので、良い勉強になりました。
結局妻は喜んだのか
- 実は出張続きで、まだ、ちゃんと渡せていないのです。。
- フィードバックもらえたら、追記するかもしれないです。
- がんばります!
明日のおうちハック Advent Calendarは
@ksasaoさんです! TWE-liteを使いこなし、TWE-liteの電圧変化から現在温度を推定するなどの謎技術をもつksasaoさん。「空気を読むUIを作る」とのこと!