株式会社LITALICOで社内システムを担当している@daisukehです。
今回は趣味で開発した「サイクリング用GPSナビゲーション&ロギングシステム」について寄稿します。
## 開発の経緯と目的
数年前に個人的な運動不足とストレス解消のため、ロードバイクでサイクリングを始めました。休日にダーッと走り回るのはとても楽しいのですが、折角いろいろな場所を走るのなら走行記録を残したいと考えて、サイクリングナビを自作しました。以下に示すのがその仕様です。
- 現在時刻と温湿度を表示する。(GPS状態含)
- 進行方向に合わせて周辺地図を回転表示する。
- 地図には道路、鉄道、河川、送電線を表示する。
- 地図の縮尺はボタン操作と走行速度で選択する。
- 地図下部に直近の交差点名称を表示する。
- 定期的に位置情報(=走行データ)を保存する。
- 走行データはUSB経由でパソコンに転送する。
ベースにArduinoを使って、GPSユニット、温湿度センサー、SDカード、EEPROM、グラフィック液晶と、I/O拡張にCortexM0+マイコンを組み合わせました。地図情報はOpenStreetMapを利用、走行データの表示はProcessingを使いました。
## 地図情報の取得と加工
ロギングだけなら地図は必要ありませんが、現在位置を知るには地図が必要です。地図情報は幾つかのサイトから無料で入手することができます。代表的なものはこちらでしょう。
国土数値情報は道路網だけでなく、地勢情報等のとても多くの情報が提供されています。ただし、道路網については位置精度が少し荒いためナビゲーション向きではありませんし、データによってはフォーマットが独特の場合があります。一方のOpenStreetMapは、道路網の情報を中心に位置精度も細かくXMLに統一されているため、理解しやすく加工にも便利だと思います。また、実用上は問題ありませんが、私道レベルで実際とは道が違うことがありました。
OpenStreetMap Overpass APIはとても使いやすく、緯度・経度とその範囲を指定することで、数値地図のXMLを取得することが可能です。
$url = "http://overpass-api.de/api/map?bbox={$lon_bgn},{$lat_bgn},{$lon_end},{$lat_end}";
$xml = file_get_contents($url);
取得できるXMLには、緯度・経度によるポイント情報と、そのポイント情報で構成するライン情報で構成されています。(国土数値情報がソースになっている部分も見受けられます。)XMLは複雑な構造ではないため、全体をパースしなくても行単位で読み込みながら連想配列を使って整理すれば、必要な情報を簡単に抽出することができます。
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="Overpass API">
<note>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</note>
<meta osm_base="2015-04-30T16:31:02Z"/>
<bounds minlat="36.4" minlon="140" maxlat="36.5" maxlon="140.1"/>
<node id="242326042" lat="36.4126602" lon="139.9883676" version="2" timestamp="2013-12-28T14:59:49Z" changeset="19679881" uid="378532" user="nyampire"/>
<node id="242326044" lat="36.4144013" lon="139.9898159" version="1" timestamp="2008-01-22T23:10:04Z" changeset="683885" uid="19799" user="daveemtb"/>
<node id="242326046" lat="36.4164734" lon="139.9911892" version="1" timestamp="2008-01-22T23:10:04Z" changeset="683885" uid="19799" user="daveemtb"/>
<node id="242326048" lat="36.4177166" lon="139.9923908" version="1" timestamp="2008-01-22T23:10:04Z" changeset="683885" uid="19799" user="daveemtb"/>
<node id="242326050" lat="36.4202031" lon="139.9971974" version="1" timestamp="2008-01-22T23:10:04Z" changeset="683885" uid="19799" user="daveemtb"/>
…
<way id="22588377" version="7" timestamp="2013-12-28T14:59:34Z" changeset="19679881" uid="378532" user="nyampire">
<nd ref="242326065"/>
<nd ref="242326063"/>
<nd ref="242326062"/>
<nd ref="242326048"/>
<nd ref="242326046"/>
<nd ref="242326044"/>
<nd ref="242326042"/>
<tag k="layer" v="-1"/>
<tag k="name" v="五行川"/>
<tag k="name:ja" v="五行川"/>
<tag k="note" v="National-Land Numerical Information (River) 2006, MLIT Japan"/>
<tag k="note:ja" v="国土数値情報(河川データ)平成18年国土交通省"/>
<tag k="source" v="KSJ2"/>
<tag k="source_ref" v="http://nlftp.mlit.go.jp/ksj/jpgis/datalist/KsjTmplt-W05.html"/>
<tag k="waterway" v="river"/>
</way>
本プロダクトではPHPを用いてこのAPIにアクセス、取得したXMLを加工して 1byte/地点 に加工・圧縮することにしました。加工後の地図情報の精度は1地点の緯度・経度につき 0.0001°(およそ10m) です。XMLのポイント情報とライン情報を使い、地点情報をレンダリングします。
サイクリング時にグラフィック液晶で表示する地図情報から現在位置を判断できるようにするため、道路だけでなく鉄道や河川・海岸、そして送電線を表示可能にしています。道路については道路標識と照合できるように、道路番号(国道16号線等)を保持するようにしました。地図情報は縮尺別に4つのファイルで構成されています。
また、同じXMLのポイント情報から交差点の名前を抽出して、8ピクセルフォントからビットマップをレンダリング、ハッシュテーブルにマッピングしたデータを作成して、現在位置の近傍にある交差点の検索がヒットしたら、その名称画像をスクロール表示できるようにしました。
## ファームウェアの設計と開発
御存知の通りArduinoはC++的な言語で実装していくので、ペリフェラルの制御記述がとても容易と言えます。ただし、基本的なArduinoのリソースは ROM32KB+RAM2KB 16MHz と乏しく低速なため、実数演算は余程のことがない限り使わないのがセオリーですし、整数型だけだとしても必要に応じてキャスト関数を記述した方が安全です。もちろん、深い関数のネストやローカル変数の多用もご法度となります。
本プロダクトでもメモリをギリギリまで使いました。進行方向に合わせて地図を回転する処理では、SDカードから地図情報をシーケンシャルに読み出す方向と、グラフィック液晶にそのデータを画像に展開する方向を、東西南北別に用意することにしました。汎用的な処理にした場合、メモリもスピードも足りないことになるからです。以下がファームウェアの状態遷移図です。
GPSユニットと通信することで、時刻情報と位置情報を取得します。GPSユニットは衛星捕捉、衛星受信、測位処理、通信制御がインテリジェント化されているため、基本的にArduinoはデータ受信だけすればよいことになります。デフォルトでは一秒毎に各種ステータスがシリアル通信で返されるため、この情報をサイクリックに使って制御します。
$GPRMC,060149.979,A,3530.5894,N,14010.9748,E,001.0,297.4,250315,,,A*69
$GPGGA,060149.979,3530.5894,N,14010.9748,E,1,05,2.3,44.9,M,34.1,M,,0000*69
$GPRMC は時刻情報のステータスです。上記の例では 060149.979 が時刻、250315 が日付となります。時刻はGMTで返されるため、日本時間にするには9時間を加算する必要があります。
$GPGGA は位置情報のステータスです。先頭に時刻情報があり、3530.5894 が緯度、14010.9748 が経度となります。ただし、この緯度・経度は十進法度単位ではありません。度分秒に直すため、小数点以下を3600で割る必要があります。当初この仕組みがわからず、受信データ列が理解不能な場所を示していたため、とても混乱しました。
latitude = stoi(&tokens[2][0]) * 100000;
latitude += stoi(&tokens[2][2]) * 100000 / 60;
latitude += ((stoi(&tokens[2][5]) * 100)
+ stoi(&tokens[2][7])) * 10 / 60;
lngitude = ((stoi(&tokens[4][0]) * 10)
+ ctoi( tokens[4][2])) * 100000;
lngitude += stoi(&tokens[4][3]) * 100000 / 60;
lngitude += ((stoi(&tokens[4][6]) * 100)
+ stoi(&tokens[4][8])) * 10 / 60;
## ハードウェアの構成と組み立て
以下の回路図が本プロダクトのハードウェア構成です。Arduinoがメインプロセッサとなり、各ペリフェラルの通信制御を実行します。グラフィック液晶は8bitパラレル通信ですので、制御線も合わせるとArduinoのデジタルI/Oのほとんどを使ってしまいます。ストレージは地図情報を保持するSDカードと、走行データを記録するEEPROMで構成しました。自転車に搭載する都合、走行中の振動によりSDカードの物理的なリードミスが発生することを想定したこと、記録もするとリードライトの制御が必要になることを考慮して、走行データは確実に保存できるようにEEPROMとしました。地図情報は読み取りに失敗して一時的に表示が更新されなくなることは確率の問題ですし、サイクリング中に常時グラフィック液晶を見ているわけでもありませんので、問題はないと考えました。SDカードはSPI通信で制御します。3.3V駆動なので5Vを分圧していますが、ダイオードで降圧してもよかったかもしれません。
温湿度センサーとEEPROM、コプロセッサーとはIIC通信でデータのやり取りをします。当初は方位センサーも使う予定でしたが、ケーシングの都合で断念しました。操作ボタンと動作状態を示すブザーにはArduinoのI/Oが足りないため、LPC810(CortexM0+)をコプロセッサーとしてIICスレーブで使っています。チャタリングを考慮したボタン入力と、ブザーのコマンド鳴動のファームウェアを組み込んであります。
GPSユニットの受信ラインにAQV214というSSRが付いているのは、Arduino標準のUSARTとGPSユニットを使い分けるためです。こうすることでパソコンからテストデータを入力することができます。完成形は以下の通りです。
## 実車&実測
完成したサイクリングナビにテストデータを入力したのが以下の写真です。グラフィック液晶に表示された地図情報が、GoogleMapの同じ場所の地図と一致していることがおわかりになると思います。画面端に「14」や「15」と書かれているのは道路番号(国道15号線等)です。点線が河川、破線は鉄道です。また、画面中央下部が現在位置になっていて、その場所の近傍にある交差点「神奈川労働局鶴見基準監督署」のテロップが表示されています。(この場所に意図はまったくありません。緯度・経度のキリが良かっただけだったはずです。)ちなみに、画面下部にはGPS衛星捕捉数、自宅までの直線距離と方向を示しています。
実際にロードバイクに搭載して走行データをprocessingで可視化したのが下の画像です。走行データには緯度・経度の他に時刻や温湿度、GPS衛星の感度なども記録しています。昨年はこれを使って、地元千葉県のいろいろな場所の走行データを録りました。数時間かけて走った道のりをパソコンで確認するのは、単純に楽しいものです。
## おわりに
Qiitaなのにプログラミングっぽくない投稿になってしまいましたが、組み込み機器に合わせたOpenStreetMapのデータ加工やArduinoの組み込みプログラミングは、WEB系や業務系、ゲーム系等とはかなり毛色の違うものであり、とても興味深い知識やテクニックが多いと思いますし、RTOSを使う本格的な組み込み機器ほど敷居が高いものでもありません。本稿についてもいずれ、もう一歩踏み込んだプログラミングテクニックを紹介できたら幸いです。
明日の『LITALICO Advent Calendar 2016』は、@YudaiTsukamotoさんの「知識ゼロからエンジニアとして戦う為にやった5つの事」です。僕もエンジニアとして戦えてるかな~(笑)。