• IoT

M5StickC(マイコン)で格ゲーをつくる

タイトルを見てなにそれ?とお思いでしょうか。私もそう思います笑。
こんにちは原田です。料理植物、と来て今度はゲームです。
今回は熱を検知するサーマル(Thermal)センサーを用いて、謎の格ゲー(格闘技ゲーム)を作ります。

TL;DR ※まとめです

  • M5StickC+Thermalセンサーで波動拳!もといハンズフリーな方向キーを表現した(開発環境はUIFlow)
  • リフレッシュレート設定がUIFlowで設定できないことと、顔の熱問題に直面した
  • レイジングストームちょうムズい

動かしてみた

まず結論から。やってみました 割と楽しいwww
少し見づらいですが、白色のThermalセンサの前で手を動かすと、Webページ上に手の動きが表示されます。
手を↓→と動かすと波動拳という文字列が表示されます。他にも昇龍拳、竜巻旋風脚という文字列が表示されます。

 

はじめに

先日参加したIoTLTにおいて、ティッシュを取ると波動拳が出るという発表がありました。

そこで私が思ったのが、「波動拳 は ↓→」ということです(なんのことか分からない方はさておき)。 
私は「↓→」で波動拳を撃ちたい!ということで、実現方法を考えました。

自宅にあったセンサを探してみましたが、Joyスティックなど物理の方向キーで実現するにはあまりにもありきたりです。
Raspberry Piにこんな↓HATがあればそれは実現出来ますけど、なにか工夫をしたい。ちょっとは近未来的に作りたい。

近未来的といってもVRなどのウェアラブル機材を使うのはなにか美しくない。
なにも持たず、そう、ハンズフリーで実現できないか?などと試行錯誤(5秒)した挙げ句にある考えが浮かびました。

手を熱源として捉え、その熱源位置を把握するサーマルセンサーで「↓→」を表現できるのでは?

熱を検知するThermalセンサーの前に手を出して、その手自体をコントローラーにするというものです。
プレイヤーが突き出した手の部分が描画領域の中で最も熱い場所と捉えられればOKです。
というわけでやってみました。プロトタイプならGroveで接続できる使い勝手の良いM5StickCが適切です。PoCですもの。
イメージはこちら。

用意した機材


  • M5StickC
      ESP32-Picoが搭載されたArduinoマイコンです。ESP32のためWiFi/Bluetoothの使用が可能です。
    今回の開発環境はUIFlowで、ブラウザからGUIでプログラミングをします。
    GroveやGPIO経由でセンサーへのアクセスも可能です。今回はGrove接続でThermalセンサを用います。

  • Thermal センサ(非接触温度センサ)
      熱源を測定できるセンサーです。32x24ピクセルの赤外線アレイを持ち、各点の物体表面温度を検出します。
    Grove形式なのでM5StickCへの接続が簡単。ブレッドボードすら不要です。
    ソフトウェアエンジニアに優しいですね。

  • Raspberry Pi
      MQTTブローカーとして使います。ラズパイの機種は何でも良かったので、転がっていた2Bです。
    手っ取り早く構築するために、Node-REDを使っています。Moscaを導入するのもとても楽

システム構成

システム構成は以下の通りです。構成の中にクラウドサービスは何も使っていません。
ソースコードは一度UIFlowには上がっているものの、機材などは全てローカルNW上に構築しています。

システム構成図
以下9個の測定点から最も熱い場所をM5StickCで特定し、その場所の番号をMQTT/PublishでMQTTブローカーに送信(Publish)します。
そのMQTTブローカーを監視(Subscribe)して結果を描画するVue.jsで作成したWebアプリケーションがあります。
Webアプリケーションで送られてきたコマンドを解釈し、コマンドに応じて波動拳の表示をします。なおVue.js周りは凝っていないのでご了承ください。


MQTTブローカーを介することで、M5StickCにおける処理を「非接触温度センサを用いてデータを取得する」「最も熱い場所を特定する」「場所のデータをMQTTブローカーに送信する」に絞ることができます。
どんなコマンドだったのかという判定や、データの可視化をWebアプリケーション内で実施します。
M5StickCなどの小型で十分なCPUリソースが無いデバイスでは、多数の処理を行うのに適していません。上図のように早々にM5StickCから離脱しましょう。

システム構築手順

システム構築の手順は以下の通りです。出来上がりを細かい単位で確認しつつ進めるためにも、
以下の手順が良いでしょう。(MQTTブローカーを立てる→Publisherを作る→Subscriberを作るの順)

  • Raspberry Pi上でMQTTブローカーを立てる
  • UIFlowを用いてM5StickCで動作するコードを記載する(センサデータ取得~MQTTブローカーにPublishする)
  • MQTTブローカーからSubscribeして画面表示するアプリをMac上に作る(Vue.js&MQTT.js)

では順に説明します。


Raspberry Pi上でMQTTブローカーを立てる

Raspberry PiにNode-REDをインストールし、MQTTブローカーを動作させるところまでです。
こちらを参考にNode-REDをインストールしましょう。
右上のメニューからパレットを管理、ノードの追加に進みmqttと入力すると、node-red-contrib-mqtt-brokerがヒットしますので、ノードをインストールします。

左側のメニュー(パレット)にmosca inというノードが出現します。それをドラッグ・アンド・ドロップでフローに貼り付けます

mosca inのノードをダブルクリックしてプロパティ編集画面に進み、画面にあるとおりに入力します。WS(WebSocket Portも用います)
※ユーザ名とパスワード、それとMQTTブローカーのIPが次のUIFlow設定で必要です。忘れないようにしてください。

これでNode-RED側の手順は終了です。MQTTブローカーが正常に稼働しています。
次にUIFlowを触りましょう。

UIFlowを用いてM5StickCで動作するコードを記載する(センサデータ取得~MQTTブローカーにPublishする)

M5StickCでThermalセンサーの温度を取得し、Raspberry Pi上のMQTTブローカーに対してメッセージを送信するところまでです。
M5StickCにおける実装作業はブラウザでブロックを繋げるように構築できるUIFlowを用います。UIFlowのセットアップは省略します。
UIFlowの画面。ブロックで接続していきます。


Grove接続のThermalセンサを用いているので、画面左の「Units」からThermalセンサを選択するとそれだけでThermalセンサを用いることができます。
下図の通りに実装します。

冒頭の水色MQTTブローカーの設定のところで、サーバアドレスとユーザ名、パスワードの設定をMQTTブローカーの設定で行った内容に変えてください。MQTTportは1883です。
get_thermal_data関数でThermalセンサからの温度を取得しています。
リスト操作を経てMQTTブローカーに最も熱い点の位置を送信しています。
※MQTTの機能を使っている場合は、のRunではなく、「ダウンロード」を選択すると反映されます


MQTTブローカーからSubscribeして画面表示するアプリをMac上に作る(Vue.js&MQTT.js)

上まででM5StickCからRaspberry PiのMQTTブローカーにデータを送信出来ました。ここからはWebブラウザ上に簡易的に可視化をしましょう。
可視化のためにVue.jsとMQTT.jsを導入します。Vue(vue本体 : vue@2.6.11 (npm list vueで確認), vue-cli : @vue/cli 4.1.2 (vue --versionで確認) )がインストールされているものとします。
動作確認はmacで行っています。任意のディレクトリyour_workspaceから入力を始めましょう。プロジェクト名はmqttfighter2ですw

cd your_workspace
vue create -d mqttfighter2
cd mqttfighter2
# MQTT.jsのインストール
npm install mqtt
# ディレクトリ/ファイル作成
mkdir src/plugins
mkdir public/images
touch src/plugins/mqtt.js
# public/imagesに画像ファイル1.png, 2.png ... 9.pngを用意する
# src/plugins/mqtt.jsの修正, src/main.jsの修正, src/App.vueを以下を参照に更新する
# vue アプリケーションを実行する
npm run serve

ソースコードは以下の通りに修正します。

src/main.js

import Vue form 'vue'
import App from './App.vue'
import mqtt from './plugins/mqtt'

Vue.config.productionTip = false

new Vue({
  mqtt,
  render: h => h(App),
}).$mount('#app')

src/plugins/mqtt.js
USERNAME,PASSWORD,MQTTbroker IP Addressをそれぞれ書き換えてください。

import Vue from 'vue'
import VueMqtt from 'vue-mqtt'

const options = {
  username: 'USERNAME',
  password: 'PASSWORD'
}

Vue.use(VueMqtt, 'ws://MQTTbroker IP Address:8080', options)

export default VueMqtt

src/App.vue


<template> <div id="app"> <ul> <li v-for="(item,index) in showItems" :key="index"><img :src="item" width="30"/></li> </ul> <div v-bind:style="{ display: showHadouken}">波動拳</div> <div v-bind:style="{ display: showTatsumakisenpuukyaku}">竜巻旋風脚</div> <div v-bind:style="{ display: showShouryuuken}">昇龍拳</div> <div v-bind:style="{ display: showRaisingStorm}">レイジングストーム</div> <div v-bind:style="{ display: showScrewPileDriver}">スクリューパイルドライバー</div> </div> </template> <script> /* eslint-disable no-console */ export default { data () { return { items: [], topic: 'direction', message: '' } }, computed: { showItems: function () { return this.items.map(x => '/images/' + x + '.png') }, showHadouken: function () { return this.items.toString().indexOf('2,3,6') > 0 ? 'block' : 'none' }, showTatsumakisenpuukyaku: function () { return this.items.toString().indexOf('2,1,4') > 0 ? 'block' : 'none' }, showShouryuuken: function () { return this.items.toString().indexOf('6,2,3') > 0 ? 'block' : 'none' }, showRaisingStorm: function () { return this.items.toString().indexOf('1,6,3,2,1,4,3') > 0 ? 'block' : 'none' }, showScrewPileDriver: function () { return this.items.toString().indexOf('6,3,2,1,4,7,8,9') > 0 ? 'block' : 'none' }, }, name: 'app', methods: { onMessage (topic, subscribeMessage) { if(subscribeMessage != 0 && subscribeMessage != 5){ if(this.items.length > 10){ this.items.shift() } this.items.push(subscribeMessage) } }, onConnect () { console.log('MQTT connect') } }, created () { this.$mqtt.on('connect', this.onConnect) this.$mqtt.on('message', this.onMessage) this.$mqtt.subscribe(this.topic) } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>

ふぅ。お疲れ様でした。
これでひとまず完了です。M5StickC, Raspberry Pi, Vue.jsのアプリケーションを動作させ、Thermalセンサーの前で手をかざすと動画のようになります。

仕組み上の課題

取り組んでみてわかった、システムにおける課題です。PoCであればこれには言及しなければ(使命感

顔の温度の取り扱いと位置調整が必要

非接触温度センサはハードウェアとして、物体の表面温度を取得します。
センサの前に人が構えていますので、かざした手の他にも顔や他の部位の温度が取得できます。
UIFlowのソースコードを見るとわかるのですが、今回は単純に測定地点のうち最も温度の高い位置をMQTTブローカーに送信しています。
そのため、かざした手と顔の温度が近いと判断された場合にコマンドが意図通りにならないことになります。

顔の温度が温度センサに検知されないようにする方法を探したところ、透明なクリアファイルを顔の前にかざすというものすごくアナログな方法を見つけました。
左側:クリアファイルなし。メガネの部分の温度が低い様子がみてとれる
右側:顔の左半分をクリアファイルで隠した様子。人の温度が隠れました

しかしハンズフリーを謳っている以上は、なにかを身につけることによる解決はなんとか避けたいところです。
結局アプリケーションとしては、ユーザの顔がセンサーの中央(5番の位置)に配置されていることを前提とし、顔の温度が最も高いと判定された場合にアプリケーション側で読み捨てるということにしました。
当然ユーザの身長に合わせた初期設定が必要になります。

非接触温度の時間分解能を設定するためにUIFlowでは不足

データシートを見ると、非接触温度センサのリフレッシュレートは最大64Hz(1秒間に64回測定およそ15msecごとに測定)まで設定できるようです。
コントローラの操作のように使うのであれば64Hzあれば十分です。高橋名人ですら16Hzですから。
ただしUIFlowではリフレッシュレートを変える設定はありません。実測値での推測ですが、8Hz(125msecごとに測定)に設定されている模様です。
高リフレッシュレートにするとノイズが入りやすいので、その程度に抑えている可能性があります。
高リフレッシュレートに設定する場合は、UIFlowではなくArduino IDE上で開発していく必要があります。
非接触温度センサの時間分解能自体はユースケースに足りると思います。

MQTTブローカーの代替を考えるべきか?

MQTTブローカーには到達を保証する仕組み(QoS)があるものの、到達メッセージの順序は保証しません。
到達メッセージの順序保証がないと、せっかく入力したコマンドの順序が変わってしまいそうです。
今回はUIFlowの都合により120[msec]程度の間隔でデータが送られてくる状況でしたので、順序が前後することはありませんでした。
高リフレッシュレートでデータを取得すると、短い時間間隔でデータが送信されます。そこでデータの順序が前後するような状況になるならば、手を打つ必要があります。
順番保証をするためにはどうするか?以下にいくつか課題を含めて記載します。
1. M5StickC内からメッセージを作成する際に、時間の情報をつけてブローカーに送信する
「2020/02/14 09:00:00:000,上」のような形でデータを送信することです。メッセージ自体に順番を保証していることになるので、時間とマッチさせれば問題なさそうです。
課題はSubscriber側にかなり負担がかかることです。時間の情報を読み取って、これは処理をする/まだ処理をしないという判断ロジックを入れなければならないからです。
マイコン内のクロックの動作が正しいとしても順序の判断をしなければならず、ロジックが少し入り組みます。(Subscriberにそこまで求めることになるため実装も大変)
2. IoT向けに順序保証するプロトコルであるAMQP(Advanced Message Queuing Protocol)を使用する。
MQTTではなく、通信方式を変えてしまおうというもの。
AMQPプロトコルに対応したブローカーであるRabbitMQであれば、データを順序保証しながらPublish/Subscribeすることができるかもしれません。
課題は現時点ではUIFlowで使える通信用のブロックとしてAMQPが用意されていません(MQTTだけの用意)。
Arduino向けライブラリも十分とはいえない状況です。クライアントライブラリが整備された場合に使えるようになるかもしれませんね。
3. クラウドサービスAWSを使えばなんとかなるか?
今回は全てローカルネットワーク内で構築しましたが、クラウドサービスを使ってはどうでしょうか。マイコンから直接インターネットを介す形です。
AWSのキューサービスである、Amazon SQS(FIFO)なら順序保証ができます。コンピューティングリソースもAWSなら柔軟に設定可能です。
その際の課題はむしろマイコン側です。データがインターネット上を通るためにデータの暗号化が必要ですよね。
実はM5StickCでHTTPS通信をさせるとマイコンのCPU(ESP32)がハングアップします。本当です。
一度の暗号化処理ですら耐えきれないというそれだけ小さなCPUが搭載されているからなんですね。
マイコンの処理性能などを考えずにクラウド側だけの都合で仕様を決めてはいけませんね。ハードウェアも理由があって決めているわけですし。

その他考察


UIFlowについて


課題の点で詳細な設定をする際に不足があることを記載しましたが、概ねUIFlowのブロックプログラミングで記述できる処理は豊富です。
UIFlowにはこれ以外にも、本体内蔵の加速度/ジャイロセンサ値取得、PWM制御、UART/I2C制御も行うことができます。(UIFlowの加速度センサ値読み込みでM5StickCを家電のリモコンにした事例
PoCのコードを記述する上ではUIFlowが有効でしょう。細かい設定をしたい場合にだけArduinoに切り替えると良いのです。
尚、UIFlowのブロックで実装すると、それに対応したPython(microPython)コードが生成されます。
microPythonでコードを読みたい場合はそちらを参照すると良いでしょう。(Pythonコードを編集してもブロックプログラミング側には反映されないので注意)
アイデアを早く実現し、課題も含めて様々な知見を早く得ることのできるツールに普段から親しんでおくと良いのではないでしょうか。

Thermalセンサー以外でハンズフリーであればどう構成できるか?


一つ考えうるのは人の関節を画像から検出できるOpenPoseですね。Webカメラ+OpenPose+Jetsonあたりでしょうか。
人の関節を認識することで、↓→のコマンドを認識してくれるかもしれません。それにこの方式であれば位置調節や顔の温度に関する問題は発生しません。
ただし関節位置を推論させる部分において使用するコンピューティングリソースが増えてしまいます。小型化はその分諦める必要が出てくるかもしれませんね。
また、推論する時間を要するため、リアルタイム性が失われることも考えられます。
その他はLeap Motionでしょうか。こちらはThermalセンサ同様に機材配置位置は制限されてしまいますが、もともとがゲーム想定のデバイスのため、リアルタイム性については問題なさそうです。

Thermalセンサーは他にどのような使い方ができそうか?


既に実用化されているものも含めて、非接触温度センサーは以下のような使い道があるものと思われます。
温度をメッシュ状に取得することで、見えてくるものもありますね。

  • 従業員の体温測定(センサーの前に立って体温を簡易に測定する)
  • 個人の脈拍測定(敢えて接触させ、IRの光を散乱する酸化ヘモグロビンの増減=脈拍を測定)
  • プライバシーに抵触しない形状認識(センサーで取得できるのはそもそも温度のみ。個人を特定しないで形状を捉える。Nintendo SwitchのJoy-Conも同様か?)
  • 農作物の表面温度測定(生育状況の詳細な把握)
  • 暗視による農作物の獣害、盗難防止(ライト不要のため暗闇でも活躍する)

MQTTブローカーによるPub/Subモデルは機器連携に有効


スマートプラグと呼ばれる家電を操作する機器が発売されておりホームハックを試みている方は居ると思います。
MQTTブローカーは、そういったホームハックの起点になる上でも有効です。
Pub/Subモデルであれば既存の仕組みを壊すことなく機能を拡張できます。
そのため、「同じ処理を別の端末でも処理させる」「指示する端末が増える」というケースを既存の仕組みを変更なく実現できます。
前者は同一Topicを監視するSubscriberを追加するだけですし、後者は同一TopicにPublishすると良いだけです。

MQTTブローカーをローカルで用意するのも非常に簡単です。
上で述べたように、ラズパイにNode-REDとプラグインを入れるだけで実現するので、ぜひやってみてください。
そのうちに「MQTTブローカーが無いと生活が成り立たないです!」なんていう人が現れるかもしれません。

ここまで読んでいただきありがとうございました。


オチ

レイジングストーム(屈指の難コマンド技)のコマンドの難易度が高く、原田はまだ一度も成功していません笑

おしまい。
※この記事に載せた機材は全て私物です。

関連記事

  1. RFIDタグでホテルのカードキーシステムを再現しよう(RFIDリーダR…

  2. IoT奮闘記 ~なめこから始まるIoTシステム構築~

  3. 娘に渡すIoTデバイス(Wio LTE+GPSセンサー)

  4. RPAによるゲームの自動化 ~とあるギルドマスターの挑戦

  5. コンテナに入れてみる

  6. Nature Remoを使って憧れのスマートホーム化!

  7. ランニングコストゼロで作るスマートホームコントロール(ラズパイとNod…

  8. SORACOM LTE-M Buttonで飲み会に行くことを妻に通知す…

PAGE TOP