610t が 2021年04月28日15時09分29秒 に編集
初版
タイトルの変更
obniz walker: M5StickCとobniz Board 1Yを使った歩数ゲーム
タグの変更
obniz
M5StickC
歩数計
加速度センサー
運動不足解消
本文の変更
# はじめに 新型コロナの影響で、自宅で作業をしている人も多くなっていると思います。 自宅にこもって作業をしていると、気になるのは運動不足です。 ということで、ここではobnizOSを使って、M5StickCとobniz Board 1Yを連携させて、歩くことで運動不足を解消できるスマートホームIoTを実現したいと思います。 以下の文章では、obniz Board 1Yを1Y、M5StickC with obnizOSをM5と略します。 # デモ動画 歩数をかせぐゲームの動作の様子は、以下の動画のようになります。 @[youtube](https://youtu.be/-IB-B4HMZvs) M5で計算した歩数が1Yで利用され、1Yに接続した色々なデバイスで歩数や気温、湿度、気圧などの環境情報が表示されます。 横に設置されたUSBファンに近づくことで、ファンが起動し、しばしの涼風を受けることもできます。 # アイデアスケッチ(設計図) はじめに、以下のような簡単なスケッチを作成しました。 ![アイデアスケッチ](https://gyazo.com/f73aac5a2b4fbd1ff0e21438f6de4346/raw) とりあえず漠然と、M5で歩数を計測し、1Yで情報を表示する物を作ろうと考えていました。 # システム構成 歩数ゲームのシステム構成は、以下のようになっています。 ## 部品一覧 今回、以下のような部品でシステムを作成しました。 |部品名|役割|価格*|備考| |---|---|---:|---| |obniz Board 1Y(1Y)|情報表示|6,930|| |M5StickC with obnizOS(M5)|歩数計本体|2,816|内蔵加速度センサー、ボタン、ディスプレイを利用| |サーボモーター x 2(Servo Kit 180‘)|歩数アナログ表示|1,056|1Yに接続| |M5Stack EnvII ユニット(BMP280+SHT30)|環境情報取得|913|1Yに接続| |PIR(KP-IR612)|近接感知|(共立)990|1Yに接続| |USB fan|涼風提供|???|1Yに接続| |6連LEDアレイ|歩数差分表示用|???|1Yに接続| |WS2812B LEDアレイ|未使用|???|1Yに接続| |iMac|Node.js実行用|???|iMac Mid 2011| - 断りの無い場合は、[switch-science](https://www.switch-science.com/) 2021/04/18 調べ ## 部品接続図 各部品は、最終的には、1Yに以下のようにブレッドボードで配線しました。 左側のBMP280とSHT30は、実際にはM5Stack EnvII ユニットに同梱されているものです。 ![ブレッドボード配線図](https://i.gyazo.com/6b8871a1c26344780a8dc6325b0d6372.png) M5に関しては、内蔵の加速度センサー(IMU)とディスプレイ、ボタンスイッチを使っただけで、外部の部品は使っていません。 ## プロトタイピングの様子 作品を作る前のプロトタイピング中の様子は、以下の写真のようになります。 ![プロトタイピング中](https://gyazo.com/5395d5bc507127dc0dd6f402f7f8fce6/raw) obnizでは、ステップバイステップでセンサーやアクチュエーターを追加していきながら試作を行うのがとても簡単でした。 今回のシステム作成は、以下のような順序で行いました。 - 1Yでの環境データの取得をし、表示する。 - 歩数計関連 - M5単体での歩数計を作成する。 - M5の歩数を1Yのディスプレイで表示する。 - 歩数データをサーボでアナログ表示する(1台のサーボからはじめて、2台へ拡張)。 - 一方のサーボを歩数差分を表示するように変更する。 - [失敗]~~LEDバー(WS2812)での歩数差分の表示を追加する。~~ - LEDを使って歩数差分の表示を追加する。 - [未使用]~~WS2801B LEDアレイで情報表示?~~ - 1YへPIRとUSBファンを使った涼風機構を追加する。 # 事前準備 最初に考えた歩数計のシステムが実現できるか、事前に調査を行いました。 ## 歩数計の実現方法 はじめに、obnizで利用できるどの言語で歩数計を実現できるか検討しました。 各方法で、加速度取得時間や計算にかかる時間などの実行時間のおおよその値を測定しました。 その結果は、以下の表のようになりました。 参考までに、micro:bitやM5StackCでArduino IDEで実現したバージョンの時間も表記しています。 特に断らない限り、時間の単位はミリ秒(ms)です。 |実現方法|加速度取得時間|計算時間|ボタン状態取得時間|合計時間|備考| |---|---|---|---|---|---| |Blockプログラミング版|200-500|1-2|100-200|300-700|BlockでJavaScriptを生成して時間計測コードを追加| |クラウドHTML(JavaScript)版|100-200|1-2|100-200|200-400|| |Node.js版|10-20|1-2|10-20|20-40|| |*参考:micro:bit(makecode)版*|0-1|5|-|6|msでの計測しかできないため、測定限界以下の場合がある| |*参考:M5StickC(ArduinoIDE)版*|310us|20us|- (画面表示に18ms)|2600us(18msを含む)|時間の単位はマイクロ秒(us)| 加速度の取得やボタン状態の取得にかなりの時間がかかっていることがわかります。 BlockプログラミングがHTML版に比べて3倍程度になっているのは、一度に加速度の絶対値を取得する方法がなく、x,y,z軸の3度に分けて取得した上で計算する必要があるためです。 歩数計の実現には、最低でも100ms程度の速度が必要なため、実現方式はNode.jsを使ったものに決定しました。 ## M5のバッテリー保ちの確認 歩数計として利用するには、M5をバッテリーで動かすことが必要になります。そこで、M5のバッテリーがどれぐらい保つのか調査を行いました。 M5が満充電の状態から電源が落ちるまでの時間を、いくつかの方法で測定しました。 |方法|バッテリー持続時間(歩数)|平均時間| |---|---|---| |(ボタン)+(LED)|21:17(138),22:06(141),22:03(292)|21:49| |(LED)|22:52(235),22:33(211),21:56(325)|22:27| |両者とも無し|22:58(403),29:03(385),22:39(375)|24:53| - (ボタン): ボタンスイッチによるM5上での歩数データなどの表示処理 - (LED): 歩数がカウントされるたびにLEDを点滅させる処理 各方法であまり持続時間に変化はないですが、歩数計として持続して使うためにはあまりにも短い時間となっています。 そこで、歩数計として利用するのではなく、与えられた(バッテリー保ち)時間でどれだけ歩数を稼げるかというゲームのような利用をすることに決めました。 試しに、Nintendo switchのリングフィットアドベンチャーをプレイした時のバッテリー持続時間と歩数は、それぞれ20分10秒と897歩となりました。 ![リングフィットアドベンチャープレイ中](https://i.gyazo.com/7900f0a0bc0904d75ef19419aa77b013.jpg) # ソースコード 全てのソースコードは、 https://github.com/610t/pedometer/blob/main/obniz/node/elchika.js を参照してください。 全体のソースコードの構成は以下のようになっています。 ```javascript:全体のコード(抜粋) const Obniz = require("obniz"); var obniz1 = new Obniz("1Y_OBNIZ_ID"); obniz1.onconnect = async() => { m5 = new Obniz.M5StickC("M5_OBNIZ_ID"); //// 1Yのデバイス初期化(後述) m5.onconnect = async() => { //// M5の初期化(後述) } m5.onloop = async() => { //// 歩数計の処理(後述) } m5.onclose = async() => { m5.connect(); // M5の再接続 } obniz1.onloop = async() => { //// 1Yでの情報表示(後述) //// フィーバ機構(後述) //// PIRとファンを使った一休み機構(後述) } obniz1.onclose = async() => { m5.close(); obniz1.connect(); } } ``` 以下、各部分のコードを説明していきます。 ## M5に関するコード M5に関するコードの主な部分は、以下の通りです。 ### 歩数計測部分のコード 歩数の計測部分コードは、以下のようになります。 このアルゴリズムは、[加速度センサを使って犬用Wifi歩数計を作ってみた](https://qiita.com/DH_Ito/items/76e2bf2d8a703e88fb10)を参考にさせていただきました。 適当な回数(今回は100回)の合成加速度の平均から閾値を求め、その閾値を超えた時に歩数を加算します。 実際のアルゴリズムは、以下のようになります。 - 合成加速度を求める: $a(i)=\sqrt{a_x(i)^2+a_y(i)^2+a_z(i)^2}$ - 加速度の一定期間(ここでは、$n=100$)の平均値を求め、閾値を決める: $threshold=\frac{1}{n}\sum_{i=1}^{n}a(i)$ - 動きを判定する: $\left\{\begin{array}{ll}state=\bold{true}, & a(i) > 1.1 \times threshold\\state=\bold{false},& a(i)<0.9 \times threshold\end{array}\right.$ - 歩数を増やす: $stepcounter=stepcounter+1$: $state$が$\bold{false}$から$\bold{true}$になった時 ```javascript:M5の歩数計部分のコード(抜粋) let step = 0; // 歩数 let total = 0; // 加速度合計 let count = 1; // 加速度平均用カウント let avg = 1.25; // 平均加速度 let width = avg / 10; // 歩数カウント用閾値 let state = false; // 現在の状態 let old_state = false; // 一つ前の状態 m5.onloop = async() => { // 加速度を取得する let currentAccelVals = await m5.accelerationWait(); console.log(currentAccelVals); let x_accel = currentAccelVals.x; let y_accel = currentAccelVals.y; let z_accel = currentAccelVals.z; let accel = Math.sqrt(x_accel * x_accel + y_accel * y_accel + z_accel * z_accel); //// 100回ごとに加速度の平均値を求め、閾値を決める if (count < 100) { total += accel; count += 1; } else { avg = total / count; width = avg / 10; total = avg; count = 1; } // 閾値を超えた場合に歩数を加算する if (accel > avg + width) { state = true; } else if (accel < avg - width) { state = false; } if (!old_state && state) { step += 1; // 歩数を加算 } old_state = state; } ``` ### M5の初期化と切断時の再接続のためのコード M5が切断された時の処理ですが、``m5.onclose()``内で``m5.connect()``することで実現できました。 ただ、これだけだと内蔵I2Cの初期化に失敗するため、``m5.onconnect()``で明示的に ``m5.i2c1.start({ sda: 21, scl: 22, clock: 100000, pull: "3v", mode: "master", });`` で初期化する必要がありました。 ```javascript:M5再接続時のコード(抜粋) m5.onconnect = async() => { m5.i2c1.start({ sda: 21, scl: 22, clock: 100000, pull: "3v", mode: "master", }); // 内蔵I2C(i2c1)の明示的な初期化 await m5.setupIMUWait("SH200Q"); // IMUの初期化 } m5.onclose = async() => { m5.connect(); } ``` ## 1Y関連のコード 1Y関連のコードは、以下の通りです。 ### 1Yの初期化部分 1Yに接続したセンサーやアクチュエータは以下のように初期化します。 最初は、複数のI2C接続の環境センサから情報を取得する方法がわかりませんでした。 ``obniz.getFreeI2C()``を使って2回I2Cを初期化すると、``i2c0``と``i2c1``を利用しようとし、エラーになるためです。 そこで、以下のように明示的に``obniz1.i2c0``を指定し、これを使って初期化することで、二つ(以上)のI2Cデバイスが利用できるようになりました。 ```javascript:1Yのデバイス初期化(抜粋) obniz1.onconnect = async() => { : ////// 1Y接続デバイスの初期化 //// サーボモーター let servo = obniz1.wired("ServoMotor", { signal: 11 }); let servo2 = obniz1.wired("ServoMotor", { signal: 10 }); //// I2Cを使ってBMP280とSHT31を利用する let i2c = obniz1.i2c0; i2c.start({ mode: "master", sda: 0, scl: 1, clock: 400000 }); let bmp280 = obniz1.wired("BMP280", { sdi: 0, sck: 1, i2c: i2c }); let sht31 = obniz1.wired("SHT31", { sda: 0, scl: 1, i2c: i2c }); await bmp280.applyCalibration(); // LEDの初期化 for (let l = 0; l < NUM_OF_LEDS; l++) { led[l] = obniz1.wired("LED", { anode: (l + 2) }); } // LED WS2812Bの初期化 leds = obniz1.wired("WS2812B", { din: 7 }); : } ``` ### 1Yでの情報表示部分 情報表示に使う1Yのコードは以下のようになります。 1Yのディスプレイに、歩数と、環境センサーから得られた温度、湿度、気圧を表示します。 歩数に関しては、さらに1Yに接続した二つのサーボでメーターのようにアナログで表示します。 ```c:1Yの表示部分のコード(抜粋) // サーボモータ表示用の定数 const low_digit = 10; // サーボモーター表示用の下桁 const high_digit = 100; // サーボモーター表示用の上桁 obniz1.onloop = async() => { //// 現在の情報の表示 obniz1.display.clear(); // 画面のクリア // 歩数の表示 obniz1.display.print("Step:" + step + "\n"); // サーボモーターでの歩数の表示 let angle = 180 - 180 * (step % low_digit) / (low_digit - 1); console.log("Angle:" + angle); servo.angle(angle); let angle2 = 180 - 180 * (step / low_digit) / high_digit; if (angle2 <= 0) { angle2 = 0; } servo2.angle(angle2); // SHT30での気温と湿度の表示 let v_sht = await sht31.getAllWait(); obniz1.display.print("Temp:" + v_sht.temperature.toFixed(1) + "deg.\n"); obniz1.display.print("Hum:" + v_sht.humidity.toFixed(1) + "%\n"); // BMP380での気圧の表示 let v_bmp = await bmp280.getAllWait(); obniz1.display.print("Press:" + v_bmp.pressure.toFixed(1) + "hPa"); : } ``` ### 1Yでの一休み機構のコード PIRとファンを使った一休み機構のコードは以下のようになります。 今回利用したPIRは、パーツライブラリにないものでしたが、GPIOで出力を出すタイプのものだったため、``obniz1.io9.inputWait()``で状態を取得できました。 また、ファンはUSBで電源を取るタイプのものですが、これも``obniz1.io8.output(true);``を指定することで動かすことができました。 ```javascript:PIRとファンを使った一休み機構 // PIRとFANでの涼風用の定数 let fan_off = true; // ファンの動作状態 let pir_start; // PIRの反応スタート時間 const fan_interval = 3000; // ファン動作継続時間 obniz1.onloop = async() => { : // PIRとファンを使った一休み機構 (io9:PIR, io8:FAN) if (fan_off && await obniz1.io9.inputWait()) { fan_off = false; pir_start = Date.now(); obniz1.io8.output(true); // FANを起動 } if (!fan_off && Date.now() - pir_start > fan_interval) { fan_off = true; obniz1.io8.output(false); // FANを停止 } } ``` ### フィーバーモードのコード 目標歩数を越えると、計器類を使ってフィーバーモードになって、にぎやかに祝福します。 ```javascript:フィーバーモードのコード // 特定歩数を超えた場合のフィーバーモード if (fever == false && step >= high_digit * low_digit) { const w = 200; const lw = 20; for (let i = 0; i < 5; i++) { dstep_servo.angle(180); step_servo.angle(0); m5.led.on(); for (let l = 0; l < NUM_OF_LEDS; l++) { if (l > 0) { led[l - 1].off() } led[l].on(); await obniz1.wait(lw); } led[NUM_OF_LEDS - 1].off(); await obniz1.wait(w); dstep_servo.angle(0); step_servo.angle(180); m5.led.off(); for (let l = NUM_OF_LEDS - 1; l >= 0; l--) { if (l < NUM_OF_LEDS - 1) { led[l + 1].off() } led[l].on(); await obniz1.wait(lw); } led[0].off(); await obniz1.wait(w); } fever = true; } ``` # obnizを使ってみた感想 obnizを使ってみて、最初に驚いたのは、豊富なパーツライブラリ (https://obniz.com/ja/sdk/parts)のおかげで、たくさんのセンサーやアクチュエーターが簡単に利用できることです。 今回は、最終的にNode.jsを使ってプログラミングしましたが、プロトタイピング中はパーツライブラリの実行例を使って、あれこれと試行錯誤することができました。 あまりにも簡単なため、あれもこれもと色々なセンサーやアクチュエーターをつないだシステムになってしまいました:smile: さらに、USB接続のファンは、0.5A以上の電流を消費するのですが、これがリレーなしで、obnizのピン直結で駆動できたのに驚きました。 今回、M5と1Yの間で歩数をやりとりする必要があったのですが、特にネットワーク接続を意識しなくても、同じ歩数の変数を参照するだけで、簡単にやりとりできました。 ひとつ要望をあげるとすれば、M5で何かを作る時内蔵バッテリーで動かしたいと思うことがあるのですが、ディスプレイの輝度が明るく、これがバッテリー消費を早めていると思われることです。このため、ディスプレイの輝度を下げたり、オフにする機能があるといいなと思いました。 # おわりに 当初の目的であった歩数計の実現は無理でしたが、やってみるとこのようなゲームも面白く、運動不足解消の一助とはなるかなぁと思いました。 obnizは、こういうプロトタイプを簡単に作れるので、おすすめです!!