電車を逃さないためのRustプロジェクト
(lilymara.xyz)- サンフランシスコで車なしで移動する生活では、家を出る前に近くの停留所の次の到着時刻だけをすぐ確認できる装置のほうが実用的
- 古いKindleを脱獄して壁掛けの電子インク案内板に変え、最初はBARTのWebページのキャプチャ画像を定期的に表示していた
- MUNIまで追加すると、複数のブラウザタブ、Raspberry Piのメモリ負荷、HTTP 429が重なり、スクリーンショットベースの構成の信頼性が低下した
- Rustで書き直した版では、511.orgのStop Monitoring APIからデータを取得し、
skia-safeでKindleの解像度と8-bit色深度に合わせたPNGを直接生成する - ブラウザエンジンをなくし、キャッシュ、エラー表示、レイアウト調整まで加えて、実用に耐えるKindle公共交通表示板として完成した
家を出るときに必要な情報はシンプル
- サンフランシスコで車を使わずに移動しており、バス、電車、トラム、トロリーバス、ケーブルカー、フェリーなどの公共交通をよく利用する
- CityMapperのようなアプリは目的地案内には便利だが、家の前では「どこへ行くか」よりも各路線の次の到着時刻のほうが重要
- すでに利用する停留所と路線が分かっているなら、汎用ナビゲーションアプリの現在地・目的地モデルは必要以上に複雑
- 古いKindleをスマートホームディスプレイとして使ったMatt Healyの記事と、古いNookをiCloudフォトフレームとして使ったBen Borgersの記事から着想を得た
Kindleに画像を表示するまで
- 最初の段階はKindleを脱獄してUSBNetを有効化し、SSH接続できるようにすることだった
- その後、cronジョブでディスプレイを定期的に更新する
- mobilereadのWikiとフォーラムにあるKindleハック資料が参考資料として使われた
- BARTのReal Time Departureページは、目的の駅の次の列車と自動更新を提供しており、初期データソースに適していた
- Kindle CLIの
eipsでPNGを表示しようとしたが、最初は画像がひどく引き伸ばされたり切り取られたりして使えなかった - 原因は、Kindleの表示ツールが24-bit RGB PNGを正しく処理できず、8-bit画像のように読み込んでいたことだった
- ImageMagickの
convertで-colorspace gray -depth 8を適用すると正常に表示された - Kindleの画面サイズに合った画像と8-bit色深度が重要な条件になった
- ImageMagickの
最初の実装:Node.js、Puppeteer、Raspberry Pi
- 初期サーバーはNode.jsとPuppeteerで、BARTのWebページの必要な領域をキャプチャした
- キャプチャ画像はKindleの画面サイズと8-bitグレースケールに合わせて変換したうえで、HTTPエンドポイントから返した
- Raspberry Piでサーバーを動かし、Kindleはcronで毎分画像を取得して
eipsで表示した - BARTページ自体が自動更新をサポートしていたため、サーバー起動時にページを一度開いたままにし、リクエストごとに新しく読み込まない方式で実装した
- この版はしばらくうまく動いたが、更新が時々止まったり、サーバーがメモリ不足で再起動を必要としたりした
MUNIへの拡張で明らかになった限界
- サンフランシスコ・ベイエリアには27の公共交通事業者があり、最初の実装は地域高速鉄道であるBARTだけを扱っていた
- その後、MUNIのバスとライトレール停留所の到着時刻も一緒に見たくなった
- MUNIのWebページも停留所ごとの到着時刻を提供していたため、最初は複数の停留所ページをPuppeteerでキャプチャし、その後ImageMagickで結合する方法を試した
- 画面の7つのセクションは、それぞれPuppeteerが保持しなければならないブラウザタブに対応する
- Raspberry Piには十分なメモリがなく、Chromeは軽量ではなかった
- MUNIページは頻繁にリクエストするとHTTP 429を返す
- BARTとMUNIを一緒に表示する画面はBART単独の画面よりはるかに不安定で、一目で確認できる案内板には、より信頼できる構成が必要だった
Rustで再設計した構成
- 書き直し版ではPuppeteerを排除し、Rust HTTPサーバーがPNGを直接生成する構成に変わった
- 公開インターフェースはNode.jsサーバーと似た形に保ちつつ、内部実装は完全に変わった
- 主な構成要素は3つ
- 511.orgから次の到着時刻を取得する
- 到着時刻を8-bit PNG画像としてレンダリングする
- 生成したPNGをHTTPサーバーから返す
- HTTPサーバーにはAxumを使う
- PNGレンダリングにはrust-skiaを使う
- SkiaはChromeの基盤グラフィックライブラリでもあり、Puppeteerより少ない負荷でレンダリング技術を活用できる
- 最終的なサーバーコードは約1,200行で、重要な部分だけが整理されている
511.org Stop Monitoring APIからデータを取得する
- 511.orgのStop Monitoring APIは、ベイエリアの停留所の予想到着・出発時刻をXMLとJSONで提供する
- 使用したエンドポイントは
http://api.511.org/transit/StopMonitoring/…]という形 agencyは27のベイエリア公共交通事業者のうち1つを指定し、SF MUNIのコードはSF- APIは一度に1事業者のデータだけを取得できる
- 特定の停留所1つの到着時刻、またはその事業者の全停留所データを返す
- 複数の停留所を部分集合として一度に取得する方法は見つからなかった
- 511.org APIの制限は1時間あたり60リクエストなので、停留所ごとに何度もリクエストする方式はリアルタイム表示に適していない
- SF MUNI全体のデータをリクエストしたところ、HTTPリクエストには約5秒かかり、約27 MiBのデータが返り、整形したJSONは100万行を超えた
- 全体レスポンスには約26k件の
MonitoredStopVisit項目があったが、実際の画面に必要なのは特定の停留所と路線のLineRef、DirectionRef、DestinationName、StopPointRef、ExpectedArrivalTime、DestinationDisplayだけだった
Rustのデータ処理で遭遇した細かな問題
reqwest、serde、serde_json、chrono、tokio、eyreでHTTPリクエスト、JSON解析、時刻計算、非同期実行、エラー処理を構成した- MUNIの停留所IDは、SF MUNIのWebサイトにある停留所ページで確認できる
- たとえばPersia Ave & Paris Stにはinbound用の
15918、outbound用の15919が別々にある - MUNIの停留所は方向ごとに別IDを持つ
- たとえばPersia Ave & Paris Stにはinbound用の
- 511.orgのレスポンスはJSONの前にUTF-8 Byte Order Markを付けて返しており、そのまま
.json()で解析するとexpected value at line 1 column 1エラーが発生する .text()でレスポンスをUTF-8文字列として読むとBOMが除去され、serde_json::from_strで解析できた- フィルタリング後は、関心のある停留所の約20件の到着情報だけが残り、当初の26k件よりはるかに扱いやすくなった
LineRefとDirectionRefを基準にグループ化し、ExpectedArrivalTimeを基準にソートしたうえで、UTCタイムスタンプを「何分後」という形式に変換した
SkiaでKindle用PNGを生成する
- PNGレンダリングは
skia-safeで構成した - Kindleの画面サイズである1024x758に合う
Gray8ビットマップを作り、背景を白で塗りつぶしてからテキストと線を描画した - まず「Hello World」を出力するPNG8画像を作り、レンダリングパイプラインを確認した
- その後、ダミーの到着時刻データでinboundとoutboundの2列を持つ表形式の画像を生成した
- 実際のAPIデータと結合すると、テストデータより目的地と到着時刻が多様になり、固定レイアウトでは文字列の重なりが発生した
- すばやく反復しAPI制限を回避するため、レスポンスを
data.jsonにキャッシュする一時コードを追加した
表示レイアウトを磨き込む
- 初期の実データ画面は、行ごとに表示するデータ量が異なり、可読性が低かった
- 改善内容は次のとおり
- 到着時刻を右揃えにして表の端の余白を減らす
- 各時刻の後ろの
minを削除し、リストの末尾にだけminを付ける - 次の3台だけを表示する
- 長い路線名を考慮して目的地名を右に移動する
- その後、路線IDを丸みのある灰色の吹き出し形で表示するため、
measure_strでテキスト境界を計算した - 目的地文字列は、路線ID吹き出しの右端から15ピクセル離れた位置に配置した
- 上部には薄い灰色のヘッダーを追加し、
Muni Inbound、Muni Outboundを中央揃えで表示した
AxumでPNGを提供する
- HTTPサーバーは
Axum 0.7で作り、/stops.pngルートでPNGを返す - レスポンスヘッダーは
Content-Type: image/pngに設定する - 最初は空の本文でHTTP 200を確認した後、画像生成関数を接続し、PNGバイト列をメモリから直接返すように変更した
- Kindleで
wgetを使って画像を取得しeipsで表示すると、最初は画像が回転して切り取られる問題があった - Kindleは縦向きでの使用を基本としているため、横画面として見せるにはレンダリング前に回転行列を適用する必要があった
- 画像サイズを
(758, 1024)に反転する - キャンバスを
90.0度回転する
- 画像サイズを
実用版に追加された機能
- 実際に使っている版には次の機能が追加された
- APIデータをバックグラウンドで継続的に取得し、HTTPサーバーが高速に応答するためのキャッシュシステム
- 左下の時刻表示と右下のデータキャッシュ状態
- アンチエイリアシング
- 到着時刻領域と重ならないように目的地名を切り詰める処理
- 路線ごとに異なるグレーを使い、すばやく区別できるようにする処理
- 設定ファイルベースの構成
- エラーが発生した場合に、エラーと
.source()チェーンをPNGとしてレンダリングし、Kindle上ですぐ確認できるようにする処理
- 併せて作成した簡略化サーバーコードはtransit-kindle-simplifiedにある
- 実際の壁掛けKindleを動かしているサーバーコードはtransit-kindleにある
- BARTのスクリーンショットベース版は2023年2月ごろ動作しており、Rustで書き直した版の最終的なUI調整は2024年1月に完了した
1件のコメント
Hacker News の意見
OneBusAway の背後にある非営利団体で働いています。OneBusAway は、毎日何百万人もの人がバス・列車・トラム・ケーブルカーの位置と到着時刻を確認するために使っているオープンソースプロジェクトです
すでに OneBusAway サーバーがある都市なら、新しい SDK でカスタムアプリを作れます: https://github.com/onebusAway/?q=sdk&type=all&language=&sort...
サーバーがない都市なら、今年作成した Docker イメージと OpenTofu 設定で GTFS/GTFS-RT フィードを REST API に変換できます
BART も GTFS と GTFS-RT を提供しています: https://mobilitydatabase.org/feeds/mdb-53
米国の他の公共交通機関も、新しい連邦規則により、少なくとも静的な時刻表データは GTFS で公開しなければならない状況です
関連ソフトウェアに貢献してくれる開発者はいつでも必要で、特に1日25万人のユーザーの改善を助けてくれる iOS 開発者を切実に求めています。連絡先は aaron@onebusaway.org です
Docker: https://github.com/OneBusAway/onebusaway-docker / OpenTofu: https://github.com/OneBusAway/onebusaway-deployment
自分の都市も手順が簡単なら追加したいです。最近はライブラリと SDK のサポートにかなりエンジニアリング時間を使ったようですが、新しい貢献者が時間を投じやすいように、開発者体験のわかりやすい導線へ投資する方がよさそうに見えます
たとえばこのドキュメントはかなり複雑です: https://github.com/OneBusAway/onebusaway/wiki/Multi-Region
「4分後に到着」と表示されているのに目の前でバスが出ていったり、「5分遅れ」と言っていたのに2分早く来たりします。バスがまったく現れないこともあります
元データの問題だとは思いますが、何が原因でここまで信頼性が低くなるのか、内部事情を知りたいです。ちなみに Seattle 基準です
BART と MUNI はどちらも General Transit Feed Specification に対応しており、このデータを取得する標準的な方法があります
https://gtfs.org/documentation/overview/#gtfs-realtime
本当に気に入りました。以前 Boston で車なしで暮らしていたとき、A から B へ行く方法が複数あり、「最善」が時間や途中で立ち寄る場所によって変わるケースに対応しようとして、もっと粗末なものを自分でつなぎ合わせて作ったことがあります
たとえば出勤前に息子を学校に連れて行き、時々は登園後に朝食を食べていました。次の出発時刻を見る機能があれば、面白い場所で4歳児と歩くという非決定的な状況や、逃すと運行間隔に穴が空く列車に乗るために急ぐべきか、といった判断をずっと柔軟にできたはずです
まだ https://oeffi.schildbach.de/index.html が言及されていないのは意外です。ここで望んでいることをまさにやってくれます
https://play.google.com/store/apps/details?id=nl.skywave.ovi... / https://apps.apple.com/nl/app/ovinfo/id1144468923
https://drgl.nl/
https://www.rijdendetreinen.nl/
https://spoorkaart.mwnn.nl/
公式アプリよりずっと良く、最も必要な情報である近くのバス停と次のバスのリアルタイム時刻をすぐに表示してくれます
英国なら、駅の発車案内板を模した発車案内板デバイスを購入できる
https://ukdepartureboards.co.uk/store/product/desktop-depart...
https://tramli.ch/en
私もSFに住んでいて、同じ問題を解こうとして A Better Ride というiOSアプリを作った
周辺の停留所の公共交通機関の発車時刻だけを表示する。目標は、公共交通機関を予測しやすく、探しやすくすることでストレスを減らすこと。収益化なしで余暇に作っている個人プロジェクト
乗り換えを気にせず行ける場所を探索できるようにしてくれる
私もこの分野のハードウェアを作っていて、最近GTFSをかなり扱ったが、交通データをパースして表示するのにどれほど多くの作業が入っているかすぐに分かる
よければ、各停留所から可能な移動を要約してまとめるUX上の判断や、バックエンドについて話してみたい
Kindleを脱獄したり、画像をレンダリングしたりする必要はない
自作のWebページをKindleブラウザで開き、デバッグコマンドでKindleの「スクリーンセーバー」広告や省電力画面をオフにすればよい
ブラウザバーはKindleの片側の端に残るが、端末の向きを変えれば下または右に置ける。その代わり、天気・交通・ニュース画面を簡単な HTML/CSS/JS と好みのバックエンド言語で作り、安価なDigitalOceanの4ドルインスタンスのような場所で動かせる
https://blog.notfaqs.com/2018/06/kindle-e-reader-disable-scr...
https://www.mobileread.com/forums/showthread.php?t=198334
document.documentElement.requestFullscreen()で本当の全画面になり、ステータスバーも見えない。地域の公共交通APIがWebページから直接アクセス可能だったので、バックエンドも不要だったそれでもローカルネットワーク上のサーバーと通信させれば大丈夫そうだ
このユースケースをよく理解できていないように思う。「電車に乗りたい」のではなく、AからBへ行きたいのだ
電車が故障していたり、スト中だったり、午後1〜4時に路線が止まっていたり、運行時間外だったりするなら代替手段が欲しい。だから私にとっては、Google Mapsが提供しているもの、あるいは提供しようとしているものが必要になる
Ferry BuildingからOaklandへ行こうとしていてBARTが故障しているなら、フェリーの方がよいかもしれないし、遅い時間ならタクシーかUberしかないかもしれない
東京で渋谷から麻布十番へ行くなら、Ginza Line→Namboku Line、Hanzomon Line→Oedo Line、Yamanote Line→Namboku Line、6番バス、タクシーなどの選択肢がある
各路線の駅やバス停の間もそれぞれ3〜8分歩くので、今どこに近いかが選択に影響する。遅れているか、荷物が大きい・重いかも重要だ
例えばある場所へ行くとき、39番バスに乗って15分歩けばよいのに、Googleに聞くと乗り換え2回の経路を出してくる。運が良ければ、バス1本+徒歩と所要時間が同じくらいという程度だ
そのため、39番バスがいつ来るのか知りたければ、目的地をわざと別の場所として伝えなければならない
また私のいる地域では、Googleのリアルタイムデータは疑わしく、運行事業者や一部のサードパーティデータの方が信頼できる
リアルタイムで高度な計画を立てなければならない状況もあり、そうしたツールがあれば良いが、日常的な移動はもっと単純なツールでも十分にカバーできる
挙げられた東京の例も興味深いが、Google基準ではほとんどの公共交通の選択肢が30〜40分で、ざっくりした計画ではほぼ同等だ。タクシーはより速く、自転車もほぼ同じくらいだ
電車が故障していたりスト中だったりすれば、リアルタイムの列車一覧に予期しない空白として現れる
街を移動するときはGoogle Mapsを開いて目的地を入れるが、500回行ったことのある場所へ自宅から出かけるときには、わざわざ開かない
Disneyland内だけでの店舗での買い物や食事の体験を基準に語るのと同じくらい意味がない。Suicaをタッチするだけでも他の場所より何倍も速く、そもそも対応していない場所もある
機能は多いが、東京でどの程度対応しているかは分からない。Londonでは自宅へ帰りたいと伝えると、費用、徒歩距離、速さ、乗り換え、アクセシビリティなどを基準に最適化した複数の方法を示してくれる
再利用した Lenovo ThinkSmart View タブレット/会議室デバイスに Home Assistant と地域の交通機関連携を組み合わせて、似たようなものを使っている
利点は、はるかにすぐ使えること。もちろん Lenovo にカスタム Android ROM は入れたが、好きなものを何でも表示できるし、照明やロボット掃除機のようなものも制御できる
BART の地図を物理的な LED 表示板にした美しい製品もある: https://www.designrules.co/