工場長のブログ

日々思ったことを書いてます。

ArduinoでCANのテレメトリ with LTE-M Shield + SORACOM

仕事でCANを扱うことになりそうだったので予習を兼ねて自分の車の位置情報とエンジンの回転数、速度をクラウドにアップロードして可視化してみるというのをやってみた。

作ったものの全体像は以下のようなイメージ。クルマに載せるデバイス(canduinoと命名)はエンジンの回転数と速度をOBD2コネクタ経由でCANから、位置情報をGPSモジュールから取得し、Unified Endpoint経由でSORACOM Harvestに格納し、SORACOM Lagoonで可視化している。間にSORACOMが提供するバイナリパーサーという機能を挟むことによって、canduinoから送信されたバイナリをJSON化している。

f:id:imai-factory:20210419073036p:plain

バイスの見てくれはこんなかんじ。クルマのUSBコネクタから電源をとっていて、DB9はOBD2コネクタにつながっている。LTEのアンテナの先っぽにGPSモジュールを巻きつけているのは、いちばんショートしなさそうな場所をさがした結果w

f:id:imai-factory:20210418215700p:plainf:id:imai-factory:20210418215738p:plain

最終的にこんなかんじにSORACOM Lagoonで可視化。首都高で有明から箱崎、C1外回りを通って早稲田まで移動。10秒に1回くらい位置情報を送ってるんだけどなんか荒い。地図の問題なのかデータの問題なのかは今回は検証していない。エリアチャートは上段が時速で下段がエンジンの回転数。

f:id:imai-factory:20210418220148p:plain

CANとは

 CANとはControl Area Networkの略称で、クルマの車内ネットワークなどに利用されるBUSの規格、つまりTCP/IPでいうとL2相当の技術。特徴としては、CANにつながるノードたちは自分のデータに対してCAN IDというIDを付与しそれぞれ一定の周期でBUSに流し(ブロードキャストし)ているということ。データがほしいノードは、BUSに流れてくるデータを受信し、あらかじめ知らされているCAN IDのパケットだけをフィルタリングして処理をしていく。CANのパケットは非常に単純で、CAN IDが11bitもしくは29bit、ペイロードは等しく8byteの長さになっている。

 

クルマで利用される場合は、このBUSにエンジンの回転数やスロットルの開き具合などいろいろな情報がリアルタイムに流れている。もちろん、データ読み取りだけでなく命令の送信などにも使われているので、これを使うことでドアのロックを外せちゃったりもする。らしい。ただし、CANが定義するのはあくまでパケットのフォーマットとそのやりとり手法のみであって、パケットの中身については定義を提供していない。実際、クルマのCAN BUSを流れるパケットは、メーカーや世代、車種ごとにさまざま、かつプロプライエタリである。クルマのチューニングショップがやっているエンジンの燃焼タイミングの最適化なんかは、きっとがんばってこれをリバースエンジニアリングして、CANによる設定の上書きなどしているんだと思う。CANのデータ解析については結構いろんなひとがやっているのでこんなプロジェクトがあったりもする(例がMazdaなのは自分の車がそうだから)。CAN BUSに例えばどんなデータが流れているか興味があるひとは見てみるといいと思う。

Mazda CAN ID - OpenGarages

CANってなに?の詳細についてはこちらのリンクをみてもらうのがいいかなと思う。いままでいろいろなウェブサイトでCANとは?を調べてみたけど、これがいちばんわかりやすかった。

jp.seeedstudio.com

 

バイスを作る

バイスのベースにはArduino Unoに、その上に順番にSeeedのCAN-BUS ShieldSORACOMのLTE-M Shieldをかぶせている。あとは秋月電子のGPSモジュールをいちばんうえに。最終的なデバイスの見た目はこんなかんじ。

f:id:imai-factory:20210417173636p:plain
CAN-BUS Shieldについてググるともうひとつ、SparkFunのGPSモジュールを接続できるシールドが見つかるのだけど、このCAN BUSはなんですか?ArduinoでCAN BUSモジュールを使いましょう!というブログがとてもわかり易くCANについて説明してくれていたのでこちらを使ってみることに。なお、このブログのいいところはOBD2とCANの違いについても説明してくれているのでとてもよかった(CANもOBD2についてもそのくらい素人です)。あともう一点このシールドがよかったのは、DB9コネクタと2本線の端子台どちらでもCANが使えること。ひとまず2セット買っておけば、実際のクルマに繋がなくても開発ができる。さらに言えば、あとでわかったことだけど、SeeedのモジュールはハードウェアレベルでのCAN IDフィルタリングの機能を実装してくれているので、ソフトウェア側でフィルタリング書かなくていいので便利。

 

モノが揃ったので組み立てて、SeeedやSORACOMの提供してくれているサンプルコードをそれぞれ単体で動かすところまではサクッといったんだけど、CAN経由で取得したデータをLTEでアップロードしようとするとうまく動かない。エラーも吐かない。たぶんCANとLTEのシールドでピンが被ってるんだろうなと回路図とかいろいろ調べてみると、あたりっぽい。CANは9,11-13、LTEは10-11がほしそうで、ピン11が被ってる。ミソとかモシってなんだよ~とかぼやきながら(SPI初めて触ったのです)Seeedのwikiを見るとCANのほうはピンアサインの変更にパターンカットが必要そう。一方、LTE-M Shieldのほうは配線だけで変更できそうなのでこちらを変更することにして、結局3と4を使う形に。

 

 GPSモジュールも含めて、最終的なArduinoから見た接続は以下のような感じ(たぶん)。

f:id:imai-factory:20210417181345p:plain
なんとか動くようになったのでOBD2とDB9の変換ケーブルを使ってドキドキしながらクルマにつなげてみる・・・!

SORACOM Harvestにデータアップロードできたじゃん!!!!1

youtu.be

 

別のスケッチを使って内容をシリアルコンソールにダンプしてみるとこんなかんじでじゃんじゃんCANのデータフレームが流れてくる。

0000.092 RX: [00000000](00) 00 00 00 00 00 53 56 57 
0000.192 RX: [00000000](00) 00 00 00 00 00 53 56 58 
0000.292 RX: [00000000](00) 00 00 00 00 00 53 56 59 
0000.394 RX: [00000000](00) 00 00 00 00 00 53 56 5A 
0000.494 RX: [00000000](00) 00 00 00 00 00 53 56 5B 
0000.594 RX: [00000000](00) 00 00 00 00 00 53 56 5C 
0000.695 RX: [00000000](00) 00 00 00 00 00 53 56 5D 
0000.796 RX: [00000000](00) 00 00 00 00 00 53 56 5E 

 

CANのデータフレームを読むにはメーカーなどが提供するビットの読み取りルールが必要になるので「うん、まあSORACOM Harvestまでデータとんだし、まあ今回はここらへんまでかな・・・」と思っていたけど、もう一歩踏み込んでみることに。

CANIDについて調べてみる

mazda canid」とググってみるとさっそくこんなページ(前述のやつ)に行き当たる。

Mazda CAN ID - OpenGarages

「おおおお!」と思って手元のダンプしたデータと突き合わせてみるが、実際に流れているデータのCAN IDはこちらの表にぜんぜん見当たらない。表の最終更新日時が2016/03/13なのでしょうがないかなーと思いながらもうすこしググっているとこんな情報に突き当たる。

mx5things.blog

曰く

0x202 "Bytes 0,1" | "Bytes 2,3" | "Bytes 4,5" | "Bytes 6,7"
Engine RPM x4 | "Vehicle speed, km/h x100″ |"Throttle position, TPS% x 640" | Unknown

 とのこと。ひとまずこのとおりにCANID:202のメッセージをデコードしてみると・・・

f:id:imai-factory:20210418230239p:plain

読めた!青いグラフがエンジンの回転数で、目盛りは左側。赤が速度で目盛りは右側。家の周りをぐるっと一周走ってきた感じなので、速度はときどき0になったり(信号待ち)しながら、最大で40km/hくらい。回転数も750くらいで下限があるので、アイドルのときのことだろう。また、青と赤の時系列的推移もだいたい一致する。(エンジンの回転数がときどき0になっているのはアイドリングストップのため。)

Disclaimar

ここまで読んでいただけたらわかると思うけど、このCANパケットのデコードは「ググって見つけた情報をそのまま使ってみたら動いた」という話なのであしからず。併せて、情報をオープンにしてくれているみなさまには大変感謝。

GPSモジュールとSoftwareSerialでハマる

 CANからデータ読んでクラウドに送信して、それが読めることがわかってきたので、こんどはデバイスGPSモジュールを追加してみる。

まずハマったのが、SoftwareSerialが複数になる点。今回の構成だと、Arduinoからみてハードウェアシリアルで母艦のWindowsとつながっていて、SoftwareSerialでLTE-M Shield、GPSモジュールとつながっている(CAN ShieldはSPI)。

オフィシャルドキュメントには必要に応じて`listen()`を呼べばいいよと書いてあるが、これだとどうもうまく行かない(LTEの通信はうまくいくが、GPSモジュールのデータが読めない)。いろいろ調べていると(さまよいすぎてURLを見失ったが、Arduino Forumでこのトピックについてすごくわかりやすくて網羅的な解説をしてくれている一がいた。URL見つけたら追記する。)、以下のことがわかった。

  • ArduinoのSoftwareSerialは、複数のインスタンスがあっても、受信バッファはひとつ
  • listen()はこの受信バッファを切り替えるメソッド
  • listen()すると受信バッファがクリアされる
  • なお、受信バッファは64byte

この特性を理解したうえでこんどはGPSモジュールのマニュアルを読んでいると「このモジュールはデフォルトで1Hzで位置情報を送信する」ということがわかってくる。つまり、GPSモジュールと接続されているSoftwareSerialのポートをlistenし始めてから1秒待たないと情報が揃わない可能性があると。ということでざっくり以下のようなコードを書いたらうまく情報が得られるようになった。

gpsSerial.listen();

delay(1000);

while (gpsSerial.available() > 0) {

  // データを読み取っていろいろ処理

}

 このGPSモジュールの取り扱いについては下記のブログに大変お世話になりました。ありがとうございます!

garchiving.com

クラウド側でデータを受け取って処理する

CANとGPSのデータが取れるようになったので、あとはそれをクラウドに送っていい感じに処理してやればいいというフェーズに。ちなみにデータのパースや各種処理をデバイス側でやるという選択肢は、エッジ側に処理やロジックを持たせなくないので、今回は考えない。ということでデバイスは、おめあてのCANIDのメッセージを受け取るごとに以下の情報をバイナリとしてそのままクラウドに送信する。緯度と経度を100倍してintとして扱っているのはArduinoのコード的に扱いやすいから。(後述のSORACOMのBinary Parserで1/100して取り扱う。)

  • CANID(char array 4byte)
  • CANペイロード(byte array 8byte)
  • 緯度を100倍した値 (int 4byte )
  • 経度を100倍した値(int 4byte)

CANとGPSを読んでひとつのバイナリとしてデータを送信する最終的なデバイス側のコードは以下のとおり。

 

canduino.ino · GitHub

 

SORACOMにはBinary Parserという素敵な機能があって、固定長のバイナリを任意にパースしてJSON化することができる。上記のコードから送信されるデータを受け取るために、今回は以下のようなパーサーを定義した。詳細に興味をもってくれた方はこちらのリファレンスを見ていただければと思うが、下記のパーサーを通すと・・・

canId:0:char:4 rpm:4:uint:16:/4 speed:6:uint:16:/100 latitude:12:int:16:/100 longitude:14:int:16:/100

 

バイスからから送信されたバイナリ(16進数で表記)は・・・

30323032000000322D0000

こんなかんじにパースされてSORACOM Harvestに転送してくれる。(上記のバイナリのCANペイロードと位置情報は適当なものなので注意)

{"canId":"0202","rpm":776.75,"speed":3.05,"latitude":35.71,"longitude":139.73,"binaryParserEnabled":true}

出来上がった!

クラウドJSONでデータが保存されれば勝利!!!!今回はSORACOM Harvest + SORACOM Lagoonで可視化を実現しているが、SORACOM BeamやSORACOM FunnelでAWSGoogle Cloudに転送していろいろ処理してやることももちろん可能。ということで今回はいったんここまで。

このさき

ちなみに今回は、「Mazdaの0x202というCAN IDのメッセージ(と、GPSによる位置情報)を読み取ってクラウドに送信する」というデバイス側のコードを書いているが、もう一歩すすめるのであれば、読み取るCANIDをクラウドから取得してそのデータを送信する、くらいまでもっていけるといいなと思っている。

 

かのクリーンアーキテクチャでは「インフラストラクチャに1行でも依存したコードは”ファームウェア”だ!」と宣言されているので、今回わたしの書いたコードはMazda車の(しかも特定の世代や車種に依存した)ファームウェアということになる。よりクイックにいろいろなデータを試験的に収集することができるようになるためには、「どのデータを取得する」みたいな部分を外部から注入できるようになるといまどきっぽいね。(依存性の注入なんで「いまどき」どころではない話だが、物理デバイスの世界ではまだまだ新しい。気がする。)このへんは、例えばSORACOMのメタデータサービスを使えば実装できると思っているが、今回はてがまわっていないのでまた今度。

S+ Camera Basic + Deep SORTでリアルタイムトラッキング(前編)

今日はSORACOM Advent Calendar 2020のネタとして、S+ Camera BasicでDeep SORTを動かすことに挑戦してみたいと思います。

 

先にお断りしておきますと、残念ながら今日の時点ではS+ Camera Basic実機で動かすところまでたどり着いておらず、以下のようなシリーズ物として綴っていく予定ですorz

1. Deep SORTについてのおさらいと、まずはMacでas isで動かしてみる (←このポスト)

2. S+ Camera BasicでDeep SORTを動かす

 

Deep SORTってなに?

Deep SORTは映像のなかに映っている、複数のオブジェクト(例えば人)を同時に、かつ連続的にトラックしていくための手法のひとつで、Simple Online and Realtime Tracking with a Deep Association Metricというタイトルで論文(Wojke, Bewley, Raulus. (2017). Simple Online and Realtime Tracking with a Deep Association Metric)が投稿されており、この手法を利用することで、例えば通行量調査を実装することができます。画面の左から移動してきた人が、画面中央のボーダーラインを越えたらカウントする。また右から左に向かって移動する人に対しても同様の処理を行う、といったイメージで。

f:id:imai-factory:20201219134357p:plain

(画像参照元https://motchallenge.net/vis/MOT16-04/det/ )

 

移動体のトラッキングは単純なObject Detectionと違い、前のフレームに映っていた「あのひと」と現フレームに映っている「このひと」が同じ人かどうかを評価、同定してやる必要があります。場合によってはトラッキング対象が一時的に物陰に入ってしまったりといったような課題も出てきます。この種の問題はMulti Object Trackingと呼ばれ、MOT Challengeというコンペティションも開かれていて、上記の画像はこのMOT Challengeのデータセットのひとつです。

Deep SORTも、MOT16のデータセットに対して以下のような成績をマークしていると前述の論文中で語られています。一番したの行がDeep SORTによるデータです。

f:id:imai-factory:20201217170742p:plain

(当図表は論文より)

 

S+ Camera Basicとは?

S+ Camera Basic(サープラスカメラ、と読みます。以下、S+ Cameraと略します。)はソラコムが開発、販売するプログラマブルなネットワークカメラデバイスです(コネクティビティはもちろんSORACOM Airです!)。S+ CameraはSORACOM Mosaicという「エッジデバイス統合管理サービス」によって管理され、通信回線を通じてアプリケーションデプロイやシステムメンテナンスが行えるため、一度現場に配備したあとでも簡単にアプリケーションの更新が可能です。

このカメラの特徴としては、電源を挿すだけでオンラインになり、書き込まれたプログラムが動作し始める、プログラムはPythonで開発できる、デプロイはSORACOM Mosaic経由でオンラインで行えることが上げられます。つまり、アプリケーション開発者はPythonでの画像処理の部分、言い換えればビジネスロジックの開発にリソースを集中することができます。

なぜS+ Camera BasicでDeep SORTを?

わたしは2020年現在、株式会社ソラコムでソリューションアーキテクトとして仕事をしており、S+ Camera Basicを使ったIoTアプリケーションの設計や開発をされるお客様のお手伝いをしています。カメラを使ったIoTのユースケースとして多いのが、OCRによる文字読み取りやメータの読み取り、そして混雑度の数値化が挙げられます。混雑度の数値化については、単純なやり方としてはObject Detectionを使って、画面内に映っている人の人数を数えるというやり方があり、これはこれで有効なケースがあります。一方、人の流れを把握したい、例えば人がどこから入ってきてどこに抜けていった、というようないわゆる人流みたいなところまで取りたくなってくると、カメラでやろうとすると今回の議論にあるようなDeep SORTのような手法が必要になってきます(カメラ以外でやるなら、人の通るところにセンサーを設置していくという方法もあります。これはこれで、多くの人を同時に(かつ別々に)センシングするのが難しい、などの考慮事項もあります)。これまで仕事でDeep SORTの話をすることはちょいちょいあったのですが、自分でしっかり使い込んだことがなかったというのがなんとなく引っかかっていたので、今回、がっつり触っておこうと思ったというのが今回のモチベーションです。あとはサンプルコードとして公開しておけば後々の自分の仕事も楽になるかなというのもあり笑

Deep SORTの処理概要

さて、中身について少し見ていきましょう。前述のような問題をDeep SORTはざっくり以下のようなアプローチで解決していきます。

  1. 画面内に映っているものをObject Detectionする
  2. 検出されたオブジェクト群と前のフレームのオブジェクト群を「前フレームまでの移動量をもとに計算した現フレームでの予測位置範囲」と「見た目の類似性」という、主に2つの情報をもとに比較、同定していく

位置の予測についてはカルマンフィルタ、見た目の類似性についてはベクトル化した対象画像のコサイン類似度を使って計算をします。

1フレームごとにDeep LearningベースのObject Detectionを行い、発見された各オブジェクトに対してCNN等を活用したベクトル化、そして前フレームのオブジェクト群とのカルマンフィルタとコサイン類似度の比較を行うということになりますので「重い処理」と言っても差し支えないと思います。それでもMOT16のなかでは速いほうの部類にあるようですが(前出の図表中のRuntimeの比較にも現れていますね)、Raspberry PiのCPUだけで現実的なリアルタイムトラッキングを行うには厳しいでしょう。

ひとまず動かしてみよう

ではまずは動かしてみましょう。論文の著者であるNicolai Wojkeによる参考実装もありますがここでは下記の実装を利用してみたいと思います。詳細は後述していきますが、Object Detection部分をCoral Edge TPUのサポートを受けるTFLiteへの切り替えがしやすそうな実装だったことが理由です。

github.com

 

Tensorflow + CPU(Mac Book Pro。M1ではない) で動かしてみました。いちばん処理量の多いであろうObject DetectionのアルゴリズムはYOLOv4です。1.2〜1.3FPSという感じですね。やはりなかなか重い。Mac Book Proでこのレベルですから、S+ Camera BasicのコントローラであるRaspberry Pi4では相当厳しいでしょう。

 

次にObject Detectionの部分を前述のようにCoral Edge TPUサポート付きのTFLiteによるMobileNet SSDv2に切り替えてみました。10FPS前後出るようになっていますね!Coral Edge TPUはRaspberry Pi4でも動作がサポートされていますし、S+ Camera Basicでも動作確認が取られていますので、これは期待できそうですね。

今日はここまで。次回はS+ Camera Basicで動くようにしてみましょう

今日はこのへんで。年内にS+ Camera Basicで動かすところまで行けたらなと思っています!上記、Coral Edge TPUでDeep SORTを動かしているコードも、次回あたりに向けて公開はしていきたいと思っています。(いまはかなりmessな状態なのでちょっと無理ですw

9/30 200m坂ダッシュ x 12

前回の坂ダッシュがつらすぎたので今日はゆっくり目からスタート。ラスト1本はたぶん自己ベスト。

コンディション

  • 23℃
  • 50%
  • 8.1km/hour

タイム

  RUN REST
1 45 79
2 44 79
3 43 81
4 44 81
5 43 87
6 44 80
7 43 80
8 43 82
9 43 86
10 42 85
11 44 88
12 40  
平均 42.9 83.33333333

 

心拍

  • 平均: 144
  • 最大: 197

9/26 15kmペース走 4'00/km

いままでで一番長いペース走。ミズノのサブスリーメニューで一番きつそうなやつ。5kmを越えたあたりから余裕が出てきた。最後の1kmのそれなりにスパート効いた。

先週に引き続き、やっぱり(終わったあと)お腹壊した。負荷に対して腹筋が足りないのかな・・?

コンディション

  • 22℃
  • 82%
  • 北北東の風 10.4km/hour

タイム

  タイム 備考
1 3:56  
2 3:28 1025m
3 3:58 1025m
4 3:58 1025m
5 3:53  
6 4:01 1025m
7 4:04 1025m
8 3:59 1025m
9 3:55  
10 4:01 1025m
11 4:03 1025m
12 4:08 1025m
13 4:07 1025m
14 4:01  
15 3:51 1025m

 

心拍

  • 平均: 174
  • 最大: 192

9/19 1000mインターバル x 12 3'40/km

11本目でお腹壊して撃沈。12本目はヘロヘロでなんとかゴール。終わったらお腹痛くてトイレに駆け込み。その後も歩いて帰れずタクシーで帰宅w

コンディション

  • 23℃
  • 91%
  • 東北東の風 15.0km/hour

タイム

  RUN REST 備考
1 3:38 56  
2 3:45 65 1025m
3 3:55 72 1025m
4 3:49 74 1025m
5 3:51 81 1025m
6 3:47 87 1025m
7 3:49 82 1025m
8 3:49 80 1025m
9 3:50 120 1025m
10 3:47 84  
11 4:00 103 1025m
12 4:23   1025m


心拍

  • 平均: 171
  • 最大: 199

 

9/16 200m坂ダッシュ x 12

一本目の入りがまあまあ速かったのでそのまま頑張ったら平均タイムでベスト更新。

コンディション

  • 25℃
  • 79%
  • 風 7.5km/hour

タイム

  RUN REST
1 42 89
2 42 83
3 42 84
4 41 87
5 41 85
6 41 90
7 41 85
8 41 91
9 42 90
10 40 86
11 41 94
12 42  
平均 41.2 88

 

心拍

  • 149-198

 

9/12 12kmペース走 4'00/km

コンディション

  • 24℃
  • 87%
  • 東北東の風 10.8km/hour

タイム

  タイム 備考
1 4:03  
2 4:08 1025m
3 4:08 1025m
4 3:58 1025m
5 3:54  
6 4:03 1025m
7 4:02 1025m
8 4:00 1025m
9 3:57  
10 4:07 1025m
11 4:06 1025m
12 3:58 1025m

 

心拍

  • 161-194