hogehoge, world.

米国カリフォルニアのソフトウェアエンジニアがIT・自転車・音楽・天体写真・語学などについて書く予定。

Particle Mesh: Early Adopterはつらいよ (4) ~ ちゃんと動くMeshアプリの作り方

前回の体験レポでは「一度上がったテンションだだ下がり」という感想を書いたが、いろいろ試行錯誤して、ちゃんと動くMeshアプリケーションを書くためのポイントを幾つか見出すことができた。今回はそのあたりを説明したいと思う。まぁ本来はこのようなトリックは不要なはずで、どこかのファームアップデートで修正されて欲しいところではある。

アプリケーション例

60秒ごとにセンサー(詳細は後述)から値を読み取ってテキストにフォーマットし、クラウドにイベントとしてpublishするアプリを作ってみた。実際のコードはここ:

https://github.com/tomoto/ArduinoPlayGround/tree/master/Particle

  • tomoto_particle: アプリケーション間共通部分。実際のところコードのほとんどはここにある。アプリケーション本体をコンパイルする前にparticle library uploadでアップロードする。
  • gateway: Argon用アプリ。コンパイルflashの仕方は前回参照。
  • endpoint: Xenon用アプリ。同上。

本来であればXenonだろうがArgonだろうが全く同じコードで(Particle.publishを使って)美しく実現できるはずなのだが、前回述べた通り「Xenonとクラウド間の通信は信頼できない」という前提を置くのでひとひねり必要となる。

注意: 後日の実験でMesh用外部アンテナを使うようコードを変更した。外部アンテナを使っていない人は、gateway.inoとendpoint.inoの上の方にあるExternalAntenna ant(true)の引数をfalseに変更すること。

endpoint (Xenon側)

イベントを送る処理はこのあたり参照。XenonとArgonを区別するために#if PLATFORM_ID == PLATFORM_XENONとかPLATFORM_ARGONとか書けるらしい。Xenon用コードはこんなふうに動く。

  • クラウドへの直接通信はせず、Mesh.publishでローカルMesh Networkに対してpublishする。これならかなり信頼性高く動作する。
  • イベント名には "mesh/<デバイスID>/" というプレフィクスを付け、送信元のデバイスが識別できるようにする。本来はデバイスIDよりもデバイス名を埋め込みたいところだが、デバイス名はクラウドにアクセスしないとわからないので諦める。

またアプリケーション本体には次のトリックが入っている。

  • 冒頭にSYSTEM_THREAD(ENABLED)を書く
  • setupの最初でwaitUntil(Mesh.ready)を呼ぶ

これは何かというと、デフォルトの動作モードでは「ネットワークの確立が最後まで(クラウド接続まで)完了してからアプリケーションの処理(setup)が実行される」ので、せっかくクラウド接続なしに動ける設計にしているのが台無しになってしまうのである。そこでSYSTEM_THREAD(ENABLED)と書くことでネットワークの確立とアプリケーションの処理が並列に行われるようにし、ただMesh Networkの確立までは待ちたいのでsetupwaitUntil(Mesh.ready)を呼ぶというわけだ。

gateway (Argon側)

先述のイベントを送る処理は、Argonの場合はParticle.publishクラウドにイベントを直接送り付けるように動く。イベント名にはプレフィクス "gateway/" を付ける。

それに加えてここでやっているように、XenonからローカルMesh Network上に送られてきたプレフィクス "mesh/" のイベントをMesh.subscribeで拾い、そのままParticle.publishクラウドに転送する。

これによりArgon/Xenonからのイベントがすべてクラウドに送られるようになるが、クラウド側からはどのイベントもArgonが送信者に見えてしまうので、イベント名から本来の送信者を区別できるようにしている。つまり、Argon自身のイベント名はプレフィクスが "gateway/"、Xenonが出元のイベント名はプレフィクスが "mesh/<デバイスID>/" となる。クラウド側アプリでこれらをデバイス名あたりにマッピングできれば完璧である。

アプリケーション本体の方には、Xenon側とはまた違うトリックが入っている。

  • ApplicationWatchdogを仕込んで、5分間ハングしたら再起動するようにしている。原因不明のフリーズが一度あったため。
  • ときどきリセットガチャしたくなるので、クラウド側からリセットを掛けられるようresetSelf関数をParticle.functionで公開している。即座にリセットを実行してしまうためクラウド側から見たら絶対にリターンしてこないお行儀の悪い実装だが、テスト用なのでまあよい。

Xenonのリモートリセット

Xenonもリモートリセットしたくなることがあるのだが、Argonと同様にリセット用関数を公開してもクラウドからの直接呼び出しはまず通らない。そこでArgonからsendReset(deviceID)関数をParticle.functionで公開し、XenonのDevice IDを指定して呼び出すとMeshイベントでリセット要求を出す仕組みを入れてみた。内部的には送信(Argon)側コードがイベント "mesh/<デバイスID>/remoteReset" をMesh Networkにpublishし、それを受信(Xenon)側コードが待ち構えていてリセットがかかる仕組みになっている。これならほぼ通るので、リセットガチャが少しだけ楽になる…ことがある。

Stringを使わない

Particleのコミュニティを見ていると、どうもStringは嫌われているようである。「メモリやCPUに負荷がかかるから使わない方がいいよ」というより「String使うなんて正気か?」というトーンを感じる。動的メモリ割り当ての品質が悪くてメモリリークでもあるのだろうか。筆者は最初気にせず使っていたが、謎のArgonのフリーズがもしかしたらメモリリークかもしれないと思い、連続稼働テストでハマるのは嫌なので結局排除した。

センサーデータの中身

Mesh Networkとは直接関係ないが、このアプリケーションでどのようなデータを取得しているか、興味のある人もいるかもしれないので一応説明しておく。

  • 温度/湿度/気圧センサーBME280。おなじみのAdafruitライブラリがちゃんと動く。
  • Argon/Xenonキットについてきたフォトトランジスタによる照度センサー。フォトトランジスタを3V3とA1の間に、抵抗200kΩをA1とGND間に入れて、A1の電圧をそのまま読み取っているだけ。
  • LiPo電池の電圧。内部的にLiPo+に接続されたアナログピンがあって、それをリファレンスに書いてある通りanalogRead(BATT)で読み取ってある係数を掛けると電池の電圧がわかる。ただ、リファレンスに載っている係数は回路図に載っている抵抗値(806kΩと2MΩで分圧)から決まる値と合わないし、コミュニティでの議論とも合っていないので信用できない。結局筆者は自分の計算値を使うことにした。

実行結果

この設計により、Argon 1台 + Xenon 2台から成るネットワークからのセンサー情報が下記のようにクラウド側から見れるようになった。信頼性は評価していないが、2~3日連続稼働できる(一時的な取りこぼしや切断はあっても、フリーズや永続的な切断なくデータを送り続けて来る)程度にはなっていると思う。

f:id:tomoto335:20190106150808p:plain

このデータを読んで動くからくりを適当に構築すれば、こんな↓ふうに可視化したり、分析の結果IFTTT+Webookを叩いて「なんか無駄に暖房ついてますよ」と自分にメッセージを送ったりできる。*1

f:id:tomoto335:20190106170502p:plain

筆者は手元に浮いていたRaspberry Pi Zero WでNode-REDを動かしてこの実験システムを構築したが、今時はクラウド側インテグレーションの方法はもっとfancyなものがいくらでもあるだろう。ParticleのWebコンソールにもAzure IoT Hub / Google Cloud Platform / Webhookとの連携ボタンがついていて、イベントをそちらに流したりできるようだ。

Particle Mesh体験レポートは一旦これで一区切りだが、外部アンテナ買ってみたのでその話を次回に。

*1:外出先でメッセージを受け取ってもどうにもならなかったりするのだが、うちの暖房はリモート制御どころか電子制御すらできないので如何ともしがたい。