編集履歴一覧に戻る
hrendohのアイコン画像

hrendoh が 2021年05月13日16時32分23秒 に編集

コメント無し

本文の変更

## 概要 obniz IoTコンテスト向けに、obniz Board 1Yと距離センサーを利用した鍵締め忘れ通知システムを作ってみました。 家の鍵が閉まっているかの確認と、閉め忘れていたらスマフォの通知でお知らせしてくれます。 実際、これを実現したいと思ったらQrioなどスマートロック製品を取り付ける方が安価でかんたんな気もしますが、自宅のドアはサムターンが上下2つ付いているタイプなので、スマートロックを導入できませんでした。 とはいえ、鍵の閉め忘れが気になることが多々あるので、この機にobnizで実現することにしました。

+

## デモ動画 こちらが、実際の動きのデモです。スマフォと同時に撮りたかったので、実際に外には出ませんので鍵の閉め忘れを通知するという利用シーンのイメージではなく、あくまでシステムの動作確認のための動画になっています。 @[youtube](https://youtu.be/OtS6hEOHymg)

## 仕様 ドアの開閉状態の検出は、以下の様にドアのサムターンに対する距離を計測して距離がしきい値より小さければ鍵が「閉まっている」 ![キャプションを入力できます](https://camo.elchika.com/e703f63b525b582fac70735cbe186b1ac26e982d/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f32613935366631632d393561332d343538622d383030632d366461356637326666373832/) しきい値より大きければ「開いている」と判定します。 ![キャプションを入力できます](https://camo.elchika.com/4321ad97e66bfc36245d24c6532cd2de165ae085/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f37623631373765612d316638322d343632372d613862322d663334336138366233656265/) 距離の計測は、Firebase functionsから1分間隔で実行し、読み取った値をFirestoreに保存します。 保存した値は、Firebase hostingにホスティングされたWebアプリ (PWA) で確認することができます。 また、3回連続、つまり2〜3分間、鍵が開きっぱなしだった場合、Firebase Cloud Messageを利用してpush通知します。 ![キャプションを入力できます](https://camo.elchika.com/bedb63c8da50562e1a42856cf1e612b2a3340276/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f62386563333831622d643632312d343538652d616366302d323066346231343035343132/) システム全体の構成は以下のようになります。 ![キャプションを入力できます](https://camo.elchika.com/cfde96331ab2c5fd1baccf1984bd1d784d274d3d/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f61633837646430632d636531632d346538352d623565302d333763333830333833396533/) obniz Board 1Yは、スリープ機能を利用して省電力化し電池で動かします。 計測が3回連続で閉まっている場合は、IOトリガーを設定してスリープ、スリープからの復帰は、焦電型赤外線センサー(人感センサー)でサムターンに手をかけたタイミングで起動するようにしています。 ドアに近づいたときだけ反応するようにセンサーはあえて上向きに取り付けています。 ![キャプションを入力できます](https://camo.elchika.com/53d8efe785de3864cccad8d13fee828f9992b8a9/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f30386230333830632d326165662d346639362d613138642d363765633562306539623632/) ドアにobniz Boardなどを収めたケースをどう固定するか?ですが、こちらはコクヨの「ひっつき虫」を使ってくっつけます。 ![キャプションを入力できます](https://camo.elchika.com/c5ecb194183658063f20cb8e935e8e598386959f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f65396330366264352d393563362d343737372d393730362d333265326130383035363962/) ひっつき虫はなかなか優秀で、そこそこの重さのものもしっかり固定してくれて、かつ、剥がした後が残りません。 今回の一番の課題は、実は何でobnizをドアに固定するのか?というところだったりするので、ひっつき虫 様様です。

-

## デモ動画 こちらが、実際の動きのデモです。スマフォと同時に撮りたかったので、実際に外には出ませんので、利用シーンと言うよりはあくまでシステムの動作確認動画になっています。 <<動画>>

## 部品 距離センサー VL53L0Xは Pololuのモジュールをスイッチサイエンスから購入しました。その他細かい部品は秋月電子通商で揃います。 | 部品 | 購入サイト | |:---:|:---| | 距離センサー Pololu VL53L0X モジュール | [スイッチサイエンス](https://www.switch-science.com/catalog/3938/) | | 焦電型赤外線センサー PaPIRsVZ | [秋月電子通商](https://akizukidenshi.com/catalog/g/gM-09750/) | | 電池ボックス 単3 x 3本 Bスナップ | [秋月電子通商](https://akizukidenshi.com/catalog/g/gP-02677/) | | ジャンパーワイヤ付バッテリースナップ | [秋月電子通商](https://akizukidenshi.com/catalog/g/gP-09032/) | | ミニブレッドボード 45穴 | [秋月電子通商](https://akizukidenshi.com/catalog/g/gP-13330/) | | ジャンパーワイヤ(オス-オス)| [秋月電子通商](https://akizukidenshi.com/catalog/g/gC-05371/) | | ジャンパーワイヤー(オス-メス)| [秋月電子通商](https://akizukidenshi.com/catalog/g/gC-08933/) | | M3 x 6m 皿ネジ x 2 | [秋月電子通商](https://akizukidenshi.com/catalog/g/gP-10246/) | | M2 x 5mビス | [秋月電子通商](https://akizukidenshi.com/catalog/g/gP-15887/) | | コクヨ ひっつき虫 | [Amazon](https://www.amazon.co.jp/%E3%82%B3%E3%82%AF%E3%83%A8-%E3%81%AF%E3%81%A3%E3%81%A6%E3%81%AF%E3%81%8C%E3%81%9B%E3%82%8B-%E4%BD%95%E5%BA%A6%E3%82%82%E4%BD%BF%E3%81%88%E3%82%8B-%E3%82%BD%E3%83%95%E3%83%88%E7%B2%98%E7%9D%80%E6%9D%90-%E3%82%BF-380NX5/dp/B0012R4RPY) | ## 設計 ### 配線図 使用する部品の配線は以下のとおりです。 ![キャプションを入力できます](https://camo.elchika.com/8f28b03979c0cc23a465976788e6b43de9744e40/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f31326334326438352d393263632d343364342d626536362d373536363630393865376337/) 配線図後 ![キャプションを入力できます](https://camo.elchika.com/b950932d4beb44268d7f530f12ce359004bb144f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f32623837633639352d356461632d343635332d616437332d303561323937646136623132/) 焦電型赤外線センサー PaPIRsVZの足は細いのでジャンパーワイヤーの圧着具体に不安が残ります。 小さい基盤にはんだ付けしてモジュール化した方が良いかもしれません。 ### ケース 今回、部品を納めるためにFusion 360でケースをモデリングしてプリントしてみました。 STLファイルは以下のリンクで公開していますので、再現したい場合はご利用ください。 https://drive.google.com/drive/folders/14HadTAPDQGxzJ8DyDB4cvYHrjYTo_r5F?usp=sharing ![キャプションを入力できます](https://camo.elchika.com/1be2f953d6b5765c1abadf3b4470334bd9b04d62/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f38623731373439382d313438612d343836372d393439332d666434306631366432336165/) ケース設計のポイントは、この溝にブレッドボードの突起を挿し込んで固定することができます。 ![キャプションを入力できます](https://camo.elchika.com/2eea5f9f656f9c6d8500933b705155b6dd3dbf51/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f63303536386464342d343637642d346631622d396565652d323633396335666230626435/) 焦電型赤外線センサーを挿し込む丸い穴は、おそらく3Dプリンタできれいに成形できないので、プリント後リーマーなどを使って整える必要があります。 ![キャプションを入力できます](https://camo.elchika.com/ce10b943f1970768ab71e671b88e685a6b8b50ac/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39303366373261632d623236302d343836622d393834352d3564623930623665376464622f37346236616432652d336238652d343465382d396261352d613166663432666530666531/) ## ソースコード コードは、すべてFirebase上で動作するようにしています。 以下のコードは、Githubリポジトリで公開しています。 https://github.com/hrendoh/obniz-iot-check-the-lock Firebaseへのセットアップ手順はリポジトリのREADMEを参照ください。 以下は、ロジックを記述しているコードと説明です。 ### functions functionは、1分ごとに実行します。 処理の流れは、おおまかに以下のとおりです。 1. デバイスobniz Board 1Yへ接続 2. デバイスがonlineであれば、距離を計測。距離の計測はobniz.jsのパーツライブラリを利用。 3. 計測した値をFirestoreに保存 4. 過去3回の計測結果を取得して、鍵の開閉を判定。 5-1. 3回連続で閉じていたら、デバイスをIOトリガーでスリープ。 REST APIを利用。 5-2. 3回連続で空いていたら、Firebase Cloud Messageでpush通知。 6. obnizの接続をクローズして終了 詳細はソースコード内のコメントを参照ください。 ```javascript:functions/index.js const functions = require('firebase-functions'); const admin = require('firebase-admin'); const Obniz = require('obniz'); const fetch = require('node-fetch'); const { obniz_id, access_token } = functions.config().obniz; admin.initializeApp(); const db = admin.firestore(); const OBNIZ_REST_API_ENDPOINT = `https://obniz.com/obniz/${obniz_id}/api/1`; // REST APIでIOトリガースリープを実行 const sleepIOTrigger = async () => { const opt = { method: 'POST', body: JSON.stringify([ { "system": { "sleep_io_trigger": true } } ]), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${access_token}` } }; await fetch(OBNIZ_REST_API_ENDPOINT, opt); } // Firestoreに計測データを保存 const storeDistance = async (distance) => { try { console.log('storeDistance', distance); const docRef = await db.collection("logs").add({ distance: distance, timestamp: new Date() }); console.log("Document written with ID: ", docRef.id); } catch (err) { console.log('firestore error:', err); } } // 計測データをPWAにpush送信 const sendDistance = async (distance) => { const tokenSnapshot = await db.collection('tokens').orderBy('timestamp', 'desc').limit(1).get(); if (!tokenSnapshot.empty) { const doc = tokenSnapshot.docs[0]; const data = doc.data(); var payload = { data: { distance: `${distance}` } }; try { const response = await admin.messaging().sendToDevice(data.token, payload); console.log('Successfully sent message:', response); } catch (error) { console.log('Error sending message:', error); } } } // 開きっぱなしをpush通知 const sendNotification = async () => { const tokenSnapshot = await db.collection('tokens').orderBy('timestamp', 'desc').limit(1).get(); if (!tokenSnapshot.empty) { const doc = tokenSnapshot.docs[0]; const data = doc.data(); console.log(data.token, data.timestamp); var payload = { notification: { title: 'ドアが空いています', body: '閉めてください' } }; try { const response = await admin.messaging().sendToDevice(data.token, payload); console.log('Successfully sent message:', response); } catch (error) { console.log('Error sending message:', error); } } } // メインロジック 1分ごとに実行 const main = async () => { const obniz = new Obniz(obniz_id, { access_token: access_token }) // 参考: https://qiita.com/wicket/items/8b1acffdd9880e2b3637 const connected = await obniz.connectWait({ timeout: 5 }); if (!connected) { console.log('obniz is offline. skip process.'); await obniz.closeWait(); return; } console.log('Obniz connected!'); try { const sensor = obniz.wired('VL53L0X', { scl: 2, sda: 1 }); const distance = await sensor.getWait(); console.log(distance); await storeDistance(distance); await sendDistance(distance); const snapshot = await db.collection('logs').orderBy('timestamp', 'desc').limit(3).get(); let isGoingSleep = false; if (!snapshot.empty) { // 最近の計測が3回連続で閉じているか? isGoingSleep = snapshot.docs.every((doc) => { const data = doc.data(); console.log(data.distance, data.timestamp); return data.distance > 20 && data.distance < 120; }); } if (isGoingSleep) { // リセットのデータ -1 を保存して、IOトリガーでスリープ console.log('Key is closing, going to sleep.'); await storeDistance(-1); await sleepIOTrigger(); } else { // 3回連続で開いていたらpush通知 const isPushing = snapshot.docs.every((doc) => { const data = doc.data(); console.log(data.distance, data.timestamp); return data.distance === 20 || data.distance > 120; }); console.log('Do push: ' + isPushing); if (isPushing) { sendNotification(); } } } catch (err) { console.log(err); } finally { await obniz.closeWait(); } } exports.scheduledFunction = functions.region('asia-northeast1').pubsub.schedule('every 1 minutes') .onRun(main); ``` IOトリガーでスリープする箇所ですが、本来obniz.sleepToTriggerを使いたかったところです。ローカルでは動作するのになぜかFirebase functions上では動かず。。仕方がないのでREST APIを呼んでいます。 ### Webアプリ (PWA) PWA側は、状態の表示とpush通知の許可を求める「index.html」と、push通知を受けるサービスワーカーの「firebase-messaging-sw.js」の2つです。 ```html:public/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="robots" content="noindex"> <title>Check the Lock</title> <link rel="manifest" href="/manifest.json"> <!-- update the version number as needed --> <script defer src="/__/firebase/8.4.2/firebase-app.js"></script> <!-- include only the Firebase features as you need --> <script defer src="/__/firebase/8.4.2/firebase-firestore.js"></script> <script defer src="/__/firebase/8.4.2/firebase-messaging.js"></script> <!-- Initialize Firebase --> <script defer src="/__/firebase/init.js"></script> <style media="screen"> body { background: #ECEFF1; color: rgba(0, 0, 0, 0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; } body.open { background: #d84655; } body.close { background: #119227; } #message { max-width: 360px; margin: 200px auto 16px; padding: 32px 24px; } #message h1 { font-size: 128px; font-weight: 300; color: white; margin: 0 0 16px; text-align: center; } #load { color: rgba(0, 0, 0, 0.4); text-align: center; font-size: 13px; } </style> </head> <body> <div id="message"> <h1 id="status"></h1> </div> <p id="load">Firebase SDK Loading&hellip;</p> <script> document.addEventListener('DOMContentLoaded', async function () { const loadEl = document.querySelector('#load'); try { let app = firebase.app(); let features = [ 'firestore', 'messaging', ].filter(feature => typeof app[feature] === 'function'); loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`; // 初回はFirestoreの最新データを使ってステータスを表示 const db = firebase.firestore(); const snapshot = await db.collection('logs').orderBy('timestamp', 'desc').limit(1).get(); if (!snapshot.empty) { const doc = snapshot.docs[0]; const data = doc.data(); console.log(data); showStatus(data.distance); }; // フォアグラウンドでpushメッセージを受け取ったときの処理 const messaging = firebase.messaging(); messaging.onMessage((payload) => { console.log('Message received. ', payload); if (payload.data) { // 計測した距離を受信したらステータスに反映 const distance = parseInt(payload.data.distance); showStatus(distance); } if (payload.notification) { // フォアグラウンドで通知を受け取ったら、通知を表示 alert(`${payload.notification.title}\n${payload.notification.body}`); } }); // 通知の購読を開始 initMessaging(messaging); } catch (e) { console.error(e); loadEl.textContent = 'Error loading the Firebase SDK, check the console.'; } }); function showStatus(distance) { const statusEl = document.getElementById('status'); const status = distance > 120 || distance === 20 ? '開' : '閉'; statusEl.innerText = status; if (status === '開') { document.body.classList.add('open'); document.body.classList.remove('close'); } else { document.body.classList.add('close'); document.body.classList.remove('open'); } } function initMessaging(messaging) { registSW(); // デバイス登録トークンを取得し、Firestoreの保存 // vapidKeyは、Firebaseプロジェクトの[プロジェクトの概要]からWebアプリを開き、[Cloud Messaging]タブの[ウェブプッシュ証明書] > [鍵ペア]の値に置き換えます。 messaging.getToken({ vapidKey: '鍵ペアの値をコピー&ペースト }).then((currentToken) => { console.log(currentToken); if (currentToken) { storeToken(currentToken); } else { // Show permission request. alert('No registration token available. Request permission to generate one.'); } }).catch((err) => { alert('An error occurred while retrieving token. ' + err); }); } async function storeToken(currentToken) { const db = firebase.firestore(); const docRef = await db.collection("tokens").add({ token: currentToken, timestamp: new Date() }); console.log("Document written with ID: ", docRef.id); } function registSW() { if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register('/firebase-messaging-sw.js') .then(function (registration) { console.log('firebase-messaging-sw.js registration successful with scope: ', registration.scope); }, function (err) { alert('firebase-messaging-sw.js registration failed: ' + err); }); }); } } </script> </body> </html> ``` サービスワーカー側のコードです。 基本的には何もしていませんが、バックグラウンドで通知を受け取るのには登録が必要なので用意する必要があります。 ```javascript:public/firebase-messaging-sw.js importScripts('https://www.gstatic.com/firebasejs/8.3.2/firebase-app.js'); importScripts('https://www.gstatic.com/firebasejs/8.3.2/firebase-messaging.js'); // [プロジェクトの設定] > [全般]タブの[SDK の設定と構成]で「CDN」を選択して表示されるスクリプトに置き換える var firebaseConfig = { apiKey: "xxxxxxxxxxxxxxxxxxxx", authDomain: "your-project-id.firebaseapp.com", projectId: "your-project-id", storageBucket: "your-project-id.appspot.com", messagingSenderId: "0000000000000", appId: "1:666167191710:web:000000000000000000000" }; // Initialize Firebase firebase.initializeApp(firebaseConfig); const messaging = firebase.messaging(); messaging.onBackgroundMessage(function (payload) { console.log('[firebase-messaging-sw.js] Received background message ', payload); }); ``` ## まとめと課題 コンテストは時間制限があったため、検証結果断念したこともまとめておきます。 ### サーバーレスイベントを利用していない理由 もともとサーバーレスイベント (online) を利用して、「距離計測 > 1分間スリープ > 距離計測 > 1分間スリープ > 距離計測 > IOトリガーでスリープ」というような動作を実現したかったのですが、焦電型赤外線センサーのようにパスル信号が連続しがちな入力をIOトリガーに設定すると、スリープ後すぐにobniz Boardが復帰して、2回目以降の起動ではサーバーレスイベントが呼び出されずobnizが起動しっぱなしになってしまうことがわかりました。 これは、スイッチでも連打すると普通に再現します。 解決方法としては、トリガーとして設定した入力の前段で、信号を処理を入れる必要があるのですが、今回は時間的に断念しています。 ### VL53L0XのIO01を復帰トリガーに利用できないか? VL53L0Xは、IO01は距離の変化を出力するようにプログラミングできるAPIが用意されているため、今回のシステムは、実際は焦電型赤外線センサーはなくてもobniz BoardとVL53L0Xのみで実現できるはずです。 そうするとさらに部品点数を減らして、小型化ができそうです。 しかし、VL53L0X用のobniz パーツライブラリはIO01の処理については未対応です。パーツを拡張すれば良さそうですが、データシートを読み込んでI2C通信を実装するのは時間的に間に合いませんでした。 これをやろうと思うと、obnizOSのプラグインを利用して、IOのプログラムは公式のAPIで実装して、obniz Cloudを利用するような構成の方が楽そうです。