M5Stack Core2を使ってお手軽な自転車用ナビを作ってみました。
M5Stack core2はタッチスクリーンを搭載しているのでスクロールもできます。
どんなやつ?
- 自転車用ナビ
- 用意したルート(青色)やポイント(緑色)を表示
- gpsやkmlから読み込み&変換してSDカード経由で読み込み
- リルート機能はなし(ロングライドは事前にルート引くからいらない)
- タッチスクリーンでスイスイ地図をスクロール可能
- 地図の拡大率を切り替え可能
- スクリーンの明るさ切り替え
- キャットアイのマウントでハンドルに簡単固定
- 組み立てはネジ回しのみで半田付けなし
背景
自転車向けのナビはサイクルコンピュータと一体になったものが市販されていますが、地図表示可能モデルは3万円程度からと価格もそれなりです1。また、安いモデルだと道の表示のみで地名が出ないものもあるようです2。ちょっと手が出にくいです。
QiitaにはRaspberry Piで市販レベルのサイコンを作った猛者もいるようですが、レベルが高すぎて再現するのはなかなかのハードルです。
サイクルコンピューターをガチで作ってみたら、割とできてしまったという話 - Qiita
そこで、もう少しお手軽なデバイスでサイクリング用のナビを作ってみました。
準備
用意したもの
-
M5Stack Core2
- M5Stack無印やgrayだとメモリが足りません。気をつけて下さい。
- M5Stack GPS module v2
- M5GO bottom module for Core2
- SDカード(16GB以下)
- M3ねじ(20mm x2本、25mm x2本)
- キャットアイ ブラケットスペーサー
組み立てはねじ回しのみで可能で、特に困らないと思います。半田付けも不要です。
あるとよいもの
-
GPSアンテナ
- 内蔵でもギリギリいけますが、外部アンテナがあった方が感度が良いです。
-
両面テープ
- GPSアンテナを固定するために3m製の両面テープを使用しました。
- モバイルバッテリ
- M5GO bottom moduleの内蔵バッテリーでも2〜3時間はいけますがあると安心。
- 一日持たせるなら2000〜4000mAhくらいあると安心。
Usage
まずはM5Stack等を組み立てます。
次にパソコン上で作業します。
git clone https://github.com/akchan/cycle_navi
# pythonの必要ライブラリをインストール
pip install -r gen_map_and_route/requirements.txt
# gpx/kmlファイルを用意します。ない場合はサンプルファイル(gen_map_and_route/sample_gpx)が利用できます。
# gpx/kmlからSDカードに書き込むためのファイルを生成します。
python gen_route_dat.py sample_gpx/tokyo_sample_route.kml
# 生成されたデータをSDカードにコピーします。
cp -r map_route /mnt/sd
umount /mnt/sd
cycle_navi/m5core2/cycle_navi/cycle_navi.ino
をarduino開発環境で開いてM5Stack Core2に書き込みます。
最後にデータを書き込んだSDカードをM5Stackにセットして準備は完了です。サイクリングへ出発しましょう!
実装の要点
ここでは実装上の要点を紹介します。
詳細なコードはgithubのリポジトリを参照して下さい。
地図
地図は国土地理院の標準地図タイル3を使用しました。道路番号や地名、地図記号など一式も掲載されていて情報量は十分です。
図 地図タイルの例(zoom level=14, x=14552, y=6451)
地図タイルは日本全国をタイルと呼ばれる256 x 256 pxの小画像に分割したもので、様々なzoom level(拡大率)があります。今回はzoom level=12, 14の地図を使用しました。
地図はSDカードに保存して利用しますが、M5Stackは16GBまでしか認識しない(?)ため日本全国の地図を常に入れておくのは容量的に難しく断念しました。ルート周囲の地図のみ(といっても関東地方がまるまる収まるくらい)をダウンロードして利用しました。
タイルをディレクトリ階層に分けて配置することでファイルシステムをハッシュとして活用できます。
地図のM5Stack実装
M5Stackは数十KBのjpg画像でも表示までに200-300msほどかかるので毎回読み込んでいるとカクカクしてしまいます。そこでJPG画像はまずspriteに読み込んでおき、更新が必要になるまではspriteの内容を画面に描画するようにすることでフレームレートを確保しました。
M5Stackのスクリーンサイズ(320x240px)は地図タイル1枚(256x256px)よりも大きいので、複数のspriteを用意しておきます。
画面をスクロールする際はspriteをシフトすることで対応します。この際、一度に複数枚の画像読み込みが発生するため、メインのloop()
関数内で読み込もうとするとフリーズしたように感じられてしまいます。今回はデュアルコアCPUが搭載されているM5Stackの特徴を活用して、freeRTOSの機能(xTaskCreatePinnedToCore()
)を使って非同期で読み込まれるようにしました。
ルート&ポイント
ルートは事前に作成したkmlファイルやgpxファイルからパソコン上でナビ用ファイルを生成して、地図と同じくSDカードに配置します。(私は普段からgoogleマイマップを使ってルートを引いているのでkmlファイルがお気に入りです。)
kmlやgpxには経度、緯度で情報が記録されているので地図タイルに合わせて変換してやります。今回は該当する地点の地図タイル名のファイル(SD/route/zoom/x/y.dat
)に、該当地点のpixel indexをバイナリで保存しています(保存部分のここでは省略)。
def conv_point_coords_to_tile_idx_list(zoom, point_coords, tile_size, point_dat_tile_margin=20):
tile_idx_list = []
for lon, lat in point_coords:
_, x_tile, y_tile = GsijAltTile.calc_coords2tile_coords(zoom, lon, lat)
x_idx, y_idx = GsijAltTile.calc_tile_idx(zoom, lon, lat, tile_size)
tile_idx_list.append([zoom, x_tile, y_tile, x_idx, y_idx])
# Add tile idx to neighbor tile to avoid chipping point circle on the screen
if x_idx < point_dat_tile_margin:
tile_idx_list.append([zoom, x_tile-1, y_tile, x_idx+tile_size, y_idx])
if tile_size - point_dat_tile_margin < x_idx:
tile_idx_list.append([zoom, x_tile+1, y_tile, x_idx-tile_size, y_idx])
if y_idx < point_dat_tile_margin:
tile_idx_list.append([zoom, x_tile, y_tile-1, x_idx, y_idx+tile_size])
if tile_size - point_dat_tile_margin < y_idx:
tile_idx_list.append([zoom, x_tile, y_tile+1, x_idx, y_idx-tile_size])
return tile_idx_list
保存する際に注意が必要なのは、地図タイルの辺縁に近いポイントは、隣接タイルのファイルにも座標を書き込んでやるということです。
M5Stackの描画は地図タイル毎にSpriteで管理されているため、辺縁に近いポイントの座標を該当タイルのみに保存してしまうと、描画された点が欠けてしまうことがあります(下図)。
おわりに
M5Stack Core2でお手軽にサイクルナビを作ってみました。
部品の調達性も良く、プラスドライバだけで組み立てできるので簡単にトライできます。
これをベースにあなただけのオリジナルなサイクルナビを作ってみてはいかがでしょうか?