東京の電車・バスをスマートグラスに「スカウター」として映す — Even G2 plugin の開発記
Even Realities G2 (スマートグラス) に、東京の電車・バス・駅・運行情報をリアルタイムで映すアプリ「東京スカウター」を作りました。
576×288 px / 16 階調モノクロ・1 文字描画ですら制約だらけのハードウェアと戦う過程と、Even Hub 公式審査で 2 回 reject を喰らって学んだ作法を中心にまとめます。
TL;DR
- 作ったもの: 東京の公共交通リアルタイム情報を Even G2 に投影する Web プラグイン (単一ソナー画面)
- データ: 公共交通オープンデータセンター (ODPT) の REST + GTFS-RT。徒歩ルート案内は OSRM
-
技術: TypeScript + Vite +
@evenrealities/even_hub_sdk+ Leaflet (companion 側) +fflate(GTFS Static 解凍) +@mapbox/vector-tile -
インフラ: AWS CloudFront + S3 + CloudFront Functions (
acl:consumerKeyを edge で注入、path rewrite)、Terraform 管理 -
見どころ:
- G2 ならではのハードウェア制約 (4-bit greyscale, 200×100 image container, LVGL 固定フォント, グリフ収録範囲) との戦い
- Even Hub 審査で 2 回 reject を喰らった内容と対処
- ソースコード: 公開後にここへ GitHub リンクを貼る予定
1. Even Realities G2 とは
Even Realities が販売しているスマートグラス。眼鏡の両レンズに micro-LED で 片眼ずつ 576×288 px / 16 階調モノクロ (緑のみ) の表示を投影できる (SDK API では両眼に同じ内容が描かれる)。カメラもスピーカーもなく、プライバシーフォーカスの設計。
| 仕様 | 値 |
|---|---|
| 表示 | 576 × 288 px / 片眼 |
| 色深度 | 4-bit grayscale (緑 16 階調) |
| 通信 | Bluetooth 5.2 |
| 入力 | テンプル touchpad (Press / Double press / Swipe up / Swipe down) |
| 音声 | 4 マイクアレイ |
| カメラ | なし |
アプリ本体は スマホ上の WebView で動かして、表示だけを BLE で G2 に投げる構造。これによって普通の Web 技術 (HTML/CSS/JS) で書ける = 開発が圧倒的に楽。
詳細は Even Hub の公式ドキュメント を参照。
2. 東京スカウターって何?
戦闘力を測るscouterではないですが(え)、視界に「今、この方向に走ってる電車・バス」「最寄りの駅・バス停」「運行情報」が浮かぶイメージ。スマホを取り出さずに、目線だけで街の交通状況を把握できる。
2.1 G2 側: 単一ソナーページ
canvas 576 × 288:
┌──────────────────────────────┬───────────────┐
│ 時刻 表示範囲 ↑北 │ sonar (top) │
│ │ 200×100 │
│ 次のバス 都営バス 都02 │ │
│ 600m 北東 3分 ├───────────────┤
│ │ sonar (bot) │
│ 次の電車 銀座線 浅草行 │ 200×100 │
│ 1.2km 北 5分 │ (縦2枚で │
│ │ 実質 200×200)│
│ 最寄駅 東京駅 200m 西 │ │
│ バス停 12件 < 200m │ │
│ │ │
│ 運行情報 平常運転 │ │
└──────────────────────────────┴───────────────┘
左側がテキスト情報、右側がソナー (中央=自分、放射状に entity の方向)。1 画面に全部入れたのがポイント (理由は後述「Hub 審査 reject 体験記」)。
2.2 タッチ操作 (極小)
- シングルタップ: 何もしない (no-op) — 画面遷移は廃止
-
ダブルタップ:
bridge.shutDownPageContainer(1)でシステムの終了確認ダイアログ (Hub 審査必須要件) -
Swipe up / down: 表示半径を循環
500 m → 1 km → 5 km → 10 km
2.3 スマホ companion 側: タップでルート案内
G2 で「あの方向にバスがあるな」と気付いたら、ペアリングしたスマホの WebView 画面で マップをタップ → 徒歩ルートが見れる:
- 背景: CartoDB Positron (白ベース・低彩度) の OpenStreetMap タイル — 街路と地名がはっきり読める
- マーカー: 駅 (青)、バス停 (グレー)、走行中バス (緑)、電車 (オレンジ)、自分 (赤)
- タップ動作: 駅 / バス停 / バス / 電車を tap → 黄色 polyline でその地点までの徒歩ルート + 距離 + 推定所要時間を表示
- ルート取得: フリーの OSRM (Open Source Routing Machine) 公開デモサーバー を CloudFront proxy 経由で叩く
3. アーキテクチャ全景
ポイント
- アプリ本体 (TS + Vite で書いた SPA) は S3 + CloudFront で配信
-
すべての外部 API は CloudFront 経由でプロキシ — Even Hub plugin 権限 (
network.whitelist) を CloudFront 1 ドメインに集約でき、レビューが格段にクリーン -
CloudFront Functions で:
-
/odpt/*のクエリにacl:consumerKeyを edge 注入 (API key をクライアントに露出させない) -
/odpt-public/*//osm-tiles/*//osrm/*のパスを各オリジン用に rewrite
-
4. G2 特有のハードウェア制約との戦い
ここがいちばん面白いパート。普通の Web 開発の感覚で挑むと、ことごとく潰される。
4.1 解像度 576×288 / 16 階調モノクロ
「お、Retina の今どきまあいけるやろ」と思って何でも詰めようとすると、情報密度設計のうまさが問われるハードルになる。文字 1 個ぶんが画面の数%を占める世界。色がない (緑だけ) ので、形状でしか entity を識別できない:
- 駅: 中空角丸スクエア
□ - バス停: 1×2 ピクセルのドット
- バス: 円
- 電車: 三角形
4.2 画像コンテナの上限 200×100 px / 1 ページ 4 個まで
「地図を 400×200 で描こう」と思ったら NG。1 個の image container は 200×100 が最大。仕方ないので 4 個並べる or 縦に 2 個積んで実質 200×200 の sonar を作るなど工夫が要る:
canvas 576 × 288:
┌──────────────────┬───────────────┐
│ │ sonar_top │ 200×100 ← image container
│ left panel ├───────────────┤
│ (text) │ sonar_bot │ 200×100 ← image container
│ │ (合計で │
│ │ 200×200相当) │
└──────────────────┴───────────────┘
4.3 LVGL 固定フォント — サイズが変えられない
G2 のテキスト描画は LVGL という組み込み GUI ライブラリの上に乗っていて、フォントは 1 種類・1 サイズ固定。プラットフォーム側から差し替え不可。これは長文の情報密度を上げたいときに辛い。
回避策: OffscreenCanvas で 9–10 px の小さい文字を fillText で焼き込み、それを PNG にして image container に流す。canvas 描画なので任意サイズの文字が出る:
function drawText(g, text, x, y, sizePx, fillStyle) {
g.fillStyle = fillStyle
g.font = `${sizePx}px "Hiragino Sans", sans-serif`
g.fillText(text, x, y)
}
4.4 LVGL の文字グリフ収録範囲が狭い
これがいちばんハマる罠。LVGL のフォントは 基本 ASCII + JIS X 0208 + ごく一部の記号 しか収録していない。よく使う以下は NG:
| Unicode | 文字 | 用途 (使いたかった) | LVGL 結果 |
|---|---|---|---|
U+25A2 |
▢ |
駅マーカー | 謎の □ プレースホルダ |
U+26A0 |
⚠ |
運行情報 warning | 謎の □ プレースホルダ + warn 多発 |
U+1F68C |
🚌 |
バスアイコン | 同上 |
U+1F686 |
🚆 |
電車アイコン | 同上 |
LVGL は描画時に glyph dsc. not found for U+26A0 のような warning をログに大量に出してくる。**「絵文字や記号類は使わない」**が安全策。利用可能な代替:
-
□(U+25A1) → OK -
●(U+25CF) → OK -
▲(U+25B2) → OK -
・(U+30FB) → OK -
↑→←↓→ OK
4.5 BLE 帯域
PNG エンコードして送るので、毎フレーム再送すると詰まる。フレームハッシュで前フレームと同じなら BLE 送信を skip することで diff だけ送る運用に。
private async sendHalf(half, id, name, buf) {
const h = hash32(buf)
if (h === this.lastHash[half]) return // ← 前回と同じならスキップ
await this.bridge.updateImageRawData(...)
this.lastHash[half] = h
}
4.6 SDK のクセ
-
createStartUpPageContainer()は 起動時 1 回しか呼べない。以降はrebuildPageContainer() - ページ切替時に古い container は teardown される — その瞬間に描画送ると
container N not foundwarn - simulator は単発タップを
{sysEvent: {eventSource: 1, eventType: undefined}}で送ってくる (実機はeventType: CLICK_EVENT) → 自前 wrapper で workaround
5. 開発スタック
中核ライブラリ
{
"dependencies": {
"@evenrealities/even_hub_sdk": "^0.0.10",
"@mapbox/vector-tile": "^2.0.4",
"fflate": "^0.8.2",
"gtfs-realtime-bindings": "^1.1.1",
"leaflet": "^1.9.0",
"pbf": "^4.0.1",
"rbush": "^4.0.1"
}
}
ポイント:
-
fflate— GTFS Static zip 展開用。jszipから置き換えた (理由は後述「Hub 審査 reject 体験記」 —new Function()をバンドルに含めるため reject されたため) -
leaflet— スマホ companion 側の地図 + ルートポリラインの描画。タップ判定もネイティブ機能 -
@mapbox/vector-tile+pbf— PLATEAU 建物 MVT のパース用 (G2 描画速度の都合で現在は描画停止中、依存だけ残)
6. データソース — ODPT
ODPT は MLIT などが後援する公共交通オープンデータ協議会が運営するデータセンター。東京メトロ / 都営 / JR 東 / つくばエクスプレス (運営: MIR) / りんかい線 (運営: TWR) / 関東バス / 関東鉄道などの駅・路線・時刻表・列車位置・運行情報を REST + GTFS-RT で提供している。
7 事業者 × 17 種類のマトリクス全体は docs/data-catalog.md 参照。「電車のリアルタイム位置」は都営しか GTFS-RT で取れないので、メトロ / JR / MIR (TX) / TWR は odpt:Train (区間 + 方向) を駅座標に近似プロットしている。
7. AWS CloudFront を edge proxy として使う
acl:consumerKey を edge で注入する CF Function
function handler(event) {
var req = event.request;
// /odpt/api/v4/... → /api/v4/... (origin 側の path に合わせる)
req.uri = req.uri.replace(/^\/odpt\//, '/');
// consumerKey を edge で注入
var qs = req.querystring;
qs['acl:consumerKey'] = { value: '${var.odpt_consumer_key}' }; // Terraform interpolation
req.querystring = qs;
return req;
}
- ODPT key をクライアントに渡さない
- Even Hub plugin の
network.whitelistを CloudFront 1 ドメインに集約 - 5 つの外部 API (ODPT 公開、ODPT 認証、CartoDB 地図タイル、OSRM ルーティング) すべて 1 ドメイン経由
キャッシュ戦略
| パス | TTL | 理由 |
|---|---|---|
/ (HTML) |
no-cache | 新版を即座に反映 |
/assets/*-[hash].js |
1 year, immutable | ハッシュ付きなので別 URL |
/odpt-public/*, /odpt/*
|
5–15s | リアルタイム性優先 |
/osm-tiles/* (CartoDB) |
1 week (max 30 days) | タイルはほぼ不変 |
/osrm/* |
1 hour | 同一 OD は同じ結果 |
8. Hub 審査リジェクト体験記 ⭐️
書き終わってから Even Hub に submit した後、2 回 reject されて学んだ作法を共有します。これから submit する人は事前にチェックしてほしい。
第 1 回 reject の指摘 3 件
1. We couldn't find bridge.shutDownPageContainer().
Please add bridge.shutDownPageContainer(1) to the double-tap handler
to enable the exit confirmation dialog.
2. Screenshot does not meet the standard.
Please re-capture using the latest simulator.
3. new Function() poses security risks.
Please refactor to remove it.
#1 — shutDownPageContainer(1) がない
Submission Guidelines の必須要件:
Root-page double-tap calls
bridge.shutDownPageContainer(1)— the system exit confirmation dialog.
Mode0(immediate exit) is not acceptable on the root page.
A custom in-app exit confirmation UI is not acceptable on the root page either.
ところが当時の私の実装では、double-tap は **「詳細ページに遷移」**になっていた。明らかにこの指針違反。
修正: シングルページ設計に切り替え (sonar の 1 画面に全情報を統合)、double-tap は問答無用で bridge.shutDownPageContainer(1) を呼ぶように:
case 'doubleClick': {
await bridge.shutDownPageContainer(1)
break
}
ついでに「map / details ページへの遷移」自体を廃止。シングルページにしたことで遷移バグ (container 10 not found 等) も同時解消した。
#2 — スクリーンショットの背景が真っ黒
最初は「G2 ってモノクロ緑なんで黒地で当然じゃ」と思って混乱したが、よく見るとアップロードした画像が 完全に空の真っ黒 だった。原因は shutDownPageContainer(1) を呼んだ直後の真っ黒フレームで撮ってしまったこと。simulator ではこの mode 1 のダイアログ UI が実装されてなくて、G2 画面が空になる。
ちなみに real device では確認ダイアログが描画される設計なので、real device で撮影できればここまでハマらなかった気もする。
修正: simulator を再起動し、Sonar が完全に表示された状態を目視確認してから撮影:
# シェル A
npm run dev
# シェル B
evenhub-simulator http://localhost:5173
# Browser ウィンドウで Debug log を確認
# "📺 Page: SONAR" が出てから
# simulator 右下のカメラアイコンをクリック
#3 — new Function() security risk
これがいちばん意外な reject。我々のソースには new Function() なんて書いてない。
調査したら、jszip パッケージの中にあった:
// node_modules/jszip/dist/jszip.js:11404 (= bundled setImmediate polyfill)
function setImmediate(callback) {
if (typeof callback !== "function") {
callback = new Function("" + callback) // ← これ
}
...
}
実際には setImmediate(function) の形でしか呼ばれないので実行されないデッドコードだが、静的解析でバンドル内の new Function( を grep するレビュー方式だと引っかかる。
修正: jszip を fflate (modern, eval-free, より軽量) に置き換え:
// 旧
import JSZip from 'jszip'
const zip = await JSZip.loadAsync(buf)
const routesCsv = await zip.file('routes.txt').async('string')
// 新
import { unzip, strFromU8 } from 'fflate'
const files = await new Promise((resolve, reject) => {
unzip(buf, {
filter: (f) => f.name === 'routes.txt' || f.name === 'trips.txt',
}, (err, out) => err ? reject(err) : resolve(out))
})
const routesCsv = strFromU8(files['routes.txt'])
unzip の filter で 必要なファイルだけ展開できるのも fflate の利点 (GTFS Static は ~70MB に展開されるが、routes.txt と trips.txt の 2 ファイルだけで足りる)。
npx vite build 後の bundle を grep で確認:
$ grep -c "new Function" dist/assets/index-*.js
0 # ← 0 件、これで通る
第 2 回 reject の指摘 2 件
3 件直して再 submit したら、また reject:
1. Unlisted URLs found in network.whitelist
— add if used, ignore if unused.
2. Screenshot failed standard validation.
Please retake using the latest simulator.
Could you check why the screenshot background is black?
#1 — whitelist に未使用 URL
私の app.json の network.whitelist には先回り的に dev / stg / prd の 3 つの CloudFront URL を入れていたが、現実には dev しか build していない:
// 旧 — stg / prd を含んでいたが両方未構築なので reject
"whitelist": [
"https://oec-tokyo-scouter.dev.tailstone.net",
"https://oec-tokyo-scouter.stg.tailstone.net", // ← 未構築
"https://oec-tokyo-scouter.tailstone.net", // ← 未構築
"https://dataodpt.blob.core.windows.net"
]
// 新 — 実際に使ってる 2 つだけ
"whitelist": [
"https://oec-tokyo-scouter.dev.tailstone.net",
"https://dataodpt.blob.core.windows.net"
]
dataodpt.blob.core.windows.net は ODPT の /files/* エンドポイントが 302 で Azure Blob Storage の SAS 署名つき URL にリダイレクトする ため、whitelist に追加が必要なケース。desc フィールドにこれを書いておくと審査がスムーズ。
#2 — また screenshot が黒
simulator の振る舞いをもう一度整理:
- 起動直後 → containers 未生成 → 黒
-
installInitial()完了 → sonar 表示 → コンテンツあり - double-tap →
shutDownPageContainer(1)→ 黒 (simulator は dialog UI を実装してない)
→ install 完了後の sonar 表示中 に撮影するのが鉄則。Debug log で 📺 Page: SONAR が出てから 5 秒くらい待って、左パネルテキスト + 右ソナー両方が見える状態で撮影。
教訓
- 公式の Submission Guidelines は事前に隅々まで読む
-
bundle 内の
new Function/evalは依存ライブラリ起因も含めて 0 にする (grep -c "new Function" dist/...) - whitelist は実際に fetch する URL だけにする (将来用に先回り NG)
- screenshot は install 完了状態の sonar を撮る (黒フレーム NG)
9. 開発フロー全体像
具体的なコマンドは次章以降。最後に Makefile で 1 行化する話 があるので、好みで使い分けて。
10. シミュレータが超便利
Even Realities が用意してくれた evenhub-simulator (公式 npm package) が本当に良くできている。実機を出さなくても 90% の見た目検証ができるので、CI 的に回せる。
セットアップ
npm install -g @evenrealities/evenhub-simulator @evenrealities/evenhub-cli
起動 + sideload
# シェル A: Vite dev server
npm run dev
# シェル B: シミュレータ
evenhub-simulator http://localhost:5173
# → 576×288 の G2 mock window が開く
実機テストは公式 CLI の QR sideload:
evenhub qr --url http://$(ipconfig getifaddr en0):5173
# ターミナル ASCII QR → Even App でスキャン → G2 投影 (HMR 対応)
screenshot の撮り方 (審査用)
1) Browser window で Debug log を確認:
- "✅ Bridge connected"
- "📍 Location: ..."
- "📡 Static: 498 駅 / 3691 バス停"
- "📺 Page: SONAR"
が全部出てから 5 秒待つ
2) "Glasses Display" window 右下のカメラアイコンをクリック
→ ~/Downloads/evenhub-screenshot-*.png に 576×288 PNG が保存
3) Finder で PNG を開いて、左パネルの text と右の sonar circle 両方が見えることを確認
→ 全黒なら NG (撮るタイミング失敗)
11. 性能チューニング — 実機で動かして初めて分かること
実機の G2 はスマホ WebView の CPU を経由して BLE で画像を送る。要因が多重なので、何が遅いか切り分けるのが大変だった。
| ボトルネック | 対策 |
|---|---|
建物ポリゴン g.fill() × 数百個 |
建物描画を削除 (一旦) |
| 鉄道線の自作 1px ループ | g.beginPath() + g.stroke() の native API に |
fillText の駅名ラベル |
削除 (情報密度より速度) |
| 1Hz periodic re-render | data-change-driven に変更 |
| PNG エンコード × 4 タイル | hash 一致で BLE 送信を skip |
| ソナーの回転 sweep バー | 撤去 (背景静止 → hash skip が効きやすく) |
data-change-driven render
ポーラーが新データを fire したタイミングだけ描画:
const renderTick = async () => {
if (renderInFlight || transitioning) { renderScheduled = true; return }
// ... heavy work ...
}
const requestRender = () => {
const since = Date.now() - lastRender
if (since < MIN_RENDER_INTERVAL_MS) {
setTimeout(() => requestRender(), MIN_RENDER_INTERVAL_MS - since)
return
}
void renderTick()
}
busPoller.subscribe(() => requestRender())
trainGtfsRt.subscribe(() => requestRender())
// 安全網: 3 秒に 1 度は無条件で再描画 (時計表示等のため)
setInterval(() => void renderTick(), 3000)
ページ遷移時の container N not found 対策
rebuildPageContainer() を await している間、古い container はもう無く、新しい container はまだ無い「真空状態」が生まれる。そこで render が走ると warn 連発。
→ transitioning フラグで install 中の render を skip:
let transitioning = false
const renderTick = async () => {
if (renderInFlight || transitioning) { renderScheduled = true; return }
// ...
}
// page install
transitioning = true
try {
await sonarPage.install(state)
} finally {
transitioning = false
requestRender()
}
12. デプロイ — AWS CLI + Terraform で全部やる
初回: インフラを Terraform で立てる
# 1. tfstate 用の S3 + DynamoDB lock
terraform -chdir=terraform/bootstrap/dev init
terraform -chdir=terraform/bootstrap/dev apply
# 2. ODPT consumer key を Secrets Manager に保管
aws secretsmanager create-secret \
--name oec-tokyo-scouter/odpt-consumer-key \
--region ap-northeast-1 \
--secret-string "<your-odpt-key>"
# 3. Terraform に key を渡して apply
export TF_VAR_odpt_consumer_key=$(aws secretsmanager get-secret-value \
--secret-id oec-tokyo-scouter/odpt-consumer-key \
--region ap-northeast-1 --query SecretString --output text)
terraform -chdir=terraform/environments/dev init
terraform -chdir=terraform/environments/dev apply
# → CloudFront + S3 + ACM + Route53 + CF Functions が 15-20 分で立つ
通常のデプロイ
# 1. ビルド
npm run build
# 2. S3 へ sync (assets は immutable, index.html は no-cache)
BUCKET=$(terraform -chdir=terraform/environments/dev output -raw plugin_s3_bucket)
aws s3 sync dist/ s3://$BUCKET/ --delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*.html"
aws s3 cp dist/index.html s3://$BUCKET/index.html \
--cache-control "no-cache, no-store, must-revalidate"
# 3. CloudFront invalidate
DIST=$(terraform -chdir=terraform/environments/dev output -raw cloudfront_distribution_id)
aws cloudfront create-invalidation --distribution-id $DIST --paths '/*'
Hub portal に登録するときは .ehpk パッケージ
evenhub pack app.json dist -o oec-tokyo-scouter-0.1.6.ehpk
# → Hub developer portal にアップロード → レビュー申請
13. 学び・気づき
制約があるほうがクリエイティブ
「16 色しか使えない」「200×100 が最大」「絵文字使えない」というハードコアな制約があると、情報の取捨選択 が研ぎ澄まされる。「これは本当に表示すべき情報か?」と毎ピクセル考えるのは普段の Web 開発と全然違う頭の使い方で楽しい。
Edge function は強い
CloudFront Functions で 5 行の JS が consumerKey 注入 + path rewrite を edge で完結させる。**「key を front に出さない」「whitelist を 1 ドメインに集約する」**という Hub 審査クリアの観点でも有効。
Hub 審査の作法は事前に読む
reject の経験から強く言える: Submission Guidelines を最初に読み込むだけで 2 回分の往復が省ける。とくに:
- ルートページ double-tap =
shutDownPageContainer(1)必須 -
new Function/evalをバンドルから完全除去 (依存ライブラリ起因も含む) -
network.whitelistは実際に fetch する URL だけ - screenshot は app が完全描画された状態で撮る
Open data + edge function + smart glasses = 楽しい組み合わせ
ODPT のような公共データを edge proxy 経由で smart glasses に流すパイプライン、技術的にも面白くて応用範囲も広い。位置情報・方角・少しの情報で価値が出るドメインなら何でも smart glasses は強い。
14. Makefile にまとめた — 開発体験の統一
ここまで CLI 直叩きで紹介してきたコマンドは、毎回手で打つには長いので、リポジトリの Makefile に 1 行のエイリアス として整理してある:
ENV ?= dev
PROJECT := oec-tokyo-scouter
APP_VERSION := $(shell node -p "require('./app.json').version")
EHPK_NAME := $(PROJECT)-$(APP_VERSION).ehpk
install: ; npm install && npm i -g @evenrealities/evenhub-simulator @evenrealities/evenhub-cli
dev: ; npm run dev
simulator: ; evenhub-simulator http://localhost:5173
qr: ; evenhub qr --url http://$$(ipconfig getifaddr en0):5173
build: ; npm run build
pack: build ; evenhub pack app.json dist -o $(EHPK_NAME)
bootstrap: ; cd terraform/bootstrap/$(ENV) && terraform init && terraform apply
tf-apply: ; cd terraform/environments/$(ENV) && terraform apply
get-key: ; @echo "export TF_VAR_odpt_consumer_key=$$(aws secretsmanager \
get-secret-value --secret-id $(PROJECT)/odpt-consumer-key \
--query SecretString --output text)"
deploy: build upload invalidate
結果として開発フローはこれだけ
# 初回
make install
echo "ODPT_CONSUMER_KEY=<key>" > .env.local
make bootstrap ENV=dev
eval "$(make get-key)"
make tf-apply ENV=dev
# 日常
make dev # シェル A
make simulator # シェル B
make qr # 実機 sideload 用 QR
# デプロイ
make deploy ENV=dev
# Hub 提出
make pack # → oec-tokyo-scouter-0.1.6.ehpk
重要:
make Xは公式機能ではなく自作 wrapper。make qrを「Even の機能」と勘違いされないように、各 target が実際に呼んでいるコマンドは npm / evenhub CLI / aws CLI / terraform の組み合わせです。
15. これから
-
本番リリース (
ENV=prd) — まずは Hub 審査を通すこと - PLATEAU の建物再導入 — 描画コストを抑える形で (LOD1 シルエットのみ等)
- AR 的な表示 — 「次のバスはこっちの方向」のような方角インジケータ
-
多言語化 —
app.jsonに既に["ja", "en"]宣言済み、UI 全体を翻訳
まとめ
スマートグラスは「制約だらけのコンピューター」だけど、その制約こそが設計を研ぎ澄ます。Web 開発の延長で組めるし、AWS / Open data / Edge function という現代的なスタックがちょうど噛み合う面白い領域だと思う。
Hub の審査要件は最初に読み込むだけで rejection サイクルがガッと減るので、これから submit する人は 第 8 章のチェックリスト を参考にしてください。
公共交通系に限らず、位置と方角と少しの情報で価値が出るドメインなら何でも smart glasses は強い。皆さんもぜひ Even G2 で何か作ってみてください。
参考リンク
Even Realities 公式
- Even Realities — 公式サイト
- Even Hub — 公式ドキュメント
-
Even Hub Developer Portal —
.ehpkをアップロードする場所 - Even Realities Support Center
- Even Realities App on App Store
Even コミュニティ
- 💬 Even Realities Discord — 困ったらここ
- 📚 nickustinov/even-g2-notes — 非公式 G2 ノート
- 🛠️ fabioglimb/even-toolkit — コミュニティ製 dev toolkit
- 🧪 brianmatzelle/even-realities-g2-glasses — TS + Vite スターター
- 🤖 sam-siavoshian/claude-code-g2 — Claude Code を G2 で動かす
- 🎮 even-realities/EvenDemoApp — 公式 demo
- 📰 Zenn — Even G2 SDK 検証メモ (日本語)
公共交通オープンデータ (ODPT)
- 公共交通オープンデータセンター (ODPT)
- ODPT 開発者サイト
- ODPT データカタログ (CKAN)
- 公共交通オープンデータチャレンジ 2025
- Toei Bus GTFS-RT
- Toei Bus GTFS Static
- 関東バス GTFS
地図 / ルーティング
- Leaflet — 公式 — スマホ companion 地図
- CartoDB Basemaps (Positron, Dark Matter)
- OpenStreetMap — タイルの元データ
- OSRM — 公式デモ — フリー徒歩ルート (driving プロファイル)
- Project PLATEAU — 国土交通省
- Indigo Lab PLATEAU 23区 建物 MVT
今回使った npm パッケージ
- @evenrealities/even_hub_sdk — G2 plugin SDK
- @evenrealities/evenhub-simulator
- @evenrealities/evenhub-cli
- leaflet — companion 地図
- fflate — eval-free zip 展開 (jszip 代替)
- @mapbox/vector-tile
- gtfs-realtime-bindings
- rbush

