みなさんこんにちは。サーバーサイドエンジニアの柿本です。
1歳3ヶ月になる私の息子は毎日快便です。
多いと1日5回くらい。おむつを替えているそばからうんちをします。
毎朝妻が保育園の連絡帳を書いてくれるのですが、
妻「昨日、Kくんはいつうんちしたっけ?」
私「お風呂入る前と寝る前だと思う。。。いや、寝てる間もしてたな。」
となります。
困ったなーと思って近所のドラッグストアにおむつを買いに行った時にこんな物を見つけました!

こ、、これは!!ダイソーで300円で買える IoT ボタン、として一時期話題になっていたリモートシャッター!!
少し値上がりしていますが、おむつに比べれば安い安い!!ということで迷わず購入しました。
それでは早速リモートシャッターでうんちの記録を残せるようにしていきます。
「うんちシャッター!」と呼ぶことにします。
ソースコード全体はこちらにあります。
目次
全体の構成
子供がうんちをしたら、おむつを替える時にボタンをポチッと押すことにします。
Slack に通知するだけだとうんち履歴が文字通り流れてしまうので、 Googleスプレッドシートにも残すようにします。
当初はリモートシャッターと Raspberry Pi を Bluetooth でつなげようと思っていたのですが、Raspberry Pi をガラクタ入れから引っ張り出してみたら、壊れていて起動しませんでした。。
気をとりなおして、自分の Mac を使うことにします。

リモートシャッターと Mac を Bluetooth で繋げて、ボタン押下の信号を受け取った Mac が IFTTT の Webhook を叩くという流れです。 IFTTT と Slack や Googleスプレッドシートとの連携はすでにしてあるものとします。
準備
構成が決まったところで開発の下準備を始めます。
MacでBluetoothを扱えるライブラリは意外と少なく、 Python だと『pybluez』『lightblue』『BluefruitLE』、 node だと『noble』『noble-mac』といったところです。
ちなみに iOS での実装については過去のしだのエントリーをご覧ください。
色々調べたり実際に使ってみたりしたのですが、 Catalina でまともに動きそうなのは noble-mac だったのでこれを使うことにします。
% npm init -y
noble-mac は公開されていないので dependencies に追記して npm install します。
"dependencies": {
"noble-mac": "https://github.com/Timeular/noble-mac.git"
}
ついでに IFTTT の Webhook を叩くので request もインストールしておきます。
% npm install % npm install request --save
実装
Bluetooth(BLE) では通信するデバイスをそれぞれ Central とPeripheral と呼びます。

イメージとしては Peripheral には Service が入っており(複数の場合もあるらしい)その中にサービスの特性としての Characreristic があります。
デバイスの検出
ひとまずリモートシャッターの Peripheral UUID がわからないと話にならないので、それを取得します。
// find_device.js
'use strict';
const noble = require('noble-mac');
const discovered = (peripheral) => {
console.log(`Device Discovered: ${ peripheral.advertisement.localName }(${ peripheral.uuid })`);
console.log(` address: ${ peripheral.address }`);
console.log(` serviceUuids: ${ peripheral.advertisement.serviceUuids }`);
console.log(` rssi: ${ peripheral.rssi }`);
console.log('-----------------');
}
noble.on('stateChange', (state) => {
if (state === 'poweredOn') {
noble.startScanning();
} else {
noble.stopScanning();
}
});
noble.on('scanStart', () => {
console.log('[scanStart]');
});
noble.on('discover', discovered);
実行してみると、 AB shutter3 というデバイスが見つかり、 Peripheral の UUID や提供する Service の UUID を確認できます。

ちなみにですが、 Service の UUID は GATT により定義されており、 1812 は Human Interface Device になります。
Characteristicのsubscribe
Peripheral UUID がわかったところで、内部の Characteristic を取得します。
ググったところ、どうやら 2a4d という Characteristic UUID でボタンの押下情報を取得できるらしいことがわかりました。ちなみに Characteristic UUID も GATT により定義されており、 2a4d は Report になります。
// subscribe_report.js
// ---- 省略 ---- //
const REPORT_CHAR = '2a4d';
const discovered = (peripheral) => {
if( peripheral.uuid == PERIPHERAL_UUID){
peripheral.on('connect', () => {
console.log('[connect]');
peripheral.discoverServices();
});
peripheral.on('disconnect', () => {
console.log('[disconnect]');
});
peripheral.on('servicesDiscover', (services) => {
services.forEach(service => {
service.on('characteristicsDiscover', (characteristics) => {
characteristics.forEach(characteristic => {
console.log('Characteristic Discovered');
console.log(` serviceUuid: ${ characteristic._serviceUuid }`);
console.log(` uuid: ${ characteristic.uuid }`);
console.log(` name: ${ characteristic.name }`);
console.log(` type: ${ characteristic.type }`);
console.log(` properties: ${ characteristic.properties }`);
if (characteristic.uuid === REPORT_CHAR) {
characteristic.on('data', (data, isNotif) => {
const jsonStr = data.toString('utf-8');
const jsonData = JSON.parse(jsonStr);
console.log(jsonData);
// TODO: ここでIFTTTにPOST
});
characteristic.subscribe((error) => {
console.log('=> Subscribe Started');
});
}
console.log('-----------------');
});
});
service.discoverCharacteristics();
});
});
peripheral.connect();
}
};
// ---- 省略 ---- //
しかし、ここで暗礁に乗り上げます。
リモートシャッターの Peripheral に含まれる全ての Service の Characteristic を書き出していますが、 2a4d という Characteristic は見つかりません。

思考錯誤をしてわかったことは、
- デバイスもしくはライブラリの問題で Report の characteristic が見つからない
- そもそもリモートシャッターと Mac の Bluetooth の接続が不安定である
- というか、すぐに切れる。切れていたかと思うと突然つながる
- リモートシャッターのボタンを押すと、つどつど接続する
苦肉の策
ボタンを押すと一瞬だけ接続されることは検知できるので、これをトリガーにすることにします。
// ifttt_on_discovered.js
// ---- 省略 ---- //
let discovered_at;
let req_options = {
uri: `https://maker.ifttt.com/trigger/${ IFTTT_WEBHOOK_EVENT }/with/key/${ IFTTT_WEBHOOK_KEY }`,
headers: {
'Content-type': 'application/json',
},
json: {
'value1': 'うんち'
}
};
const discovered = (peripheral) => {
if( peripheral.uuid === PERIPHERAL_UUID){
const elapsed = new Date() - discovered_at;
if (elapsed > 5000) {
console.log(`Device Discovered: ${ peripheral.advertisement.localName }(${ peripheral.uuid })`);
request.post(req_options, (err, res, body) => {
if (!err) {
console.log('Report Sent');
}
});
discovered_at = new Date();
}
}
}
// ---- 省略 ---- //
接続が不安定な結果、 connect と disconnect を3秒間くらいの間に大量に繰り返すので、5秒のインターバルをおくことにしました。
結果的になんとも情けない感じにはなりましたが、なんとか「うんちシャッター!」として機能するようになりました。

Google スプレッドシートにもこの通り履歴が残ります。
最後に
上の画像ではキレイに6件通知されましたが、実際にはボタンを押していないのにリモートシャッターと Mac の Bluetooth が突然つながったりもするので、息子がうんちをしていないのに通知が来ることもあります。
なんとかして Report の characteristic を subscribe できれば防げるので、これを今後の改善点と見据えて、タイトルに「その1」とつけました。
せっかくボタンが2つあるので、うんちが硬かったら大きいボタン、ゆるかったら小さいボタン、みたいにできたらいいなと思います。
ちなみにですが、弊社の連絡帳アプリのキッズリーではうんちの状態を4段階(かたい/ふつう/ゆるい/水様便)で登録することができます。
あいにく「うんちシャッター!」の販売予定はございませんので、弊社キッズリーをご利用ください。
ユニファでは 『うんち 保育をハックする』 エンジニアを募集中です!