はじめに
すべての発端は、AWS re:Invent 2025で英語に苦戦したことでした。
「読む」はClaudeのOCRやGoogle翻訳でなんとかなります。「書く」もClaudeに任せれば大丈夫。この2つはリアルタイム性が基本的に要らないので、既存のテクノロジーで十分戦えるんですよね。
問題は残りの2つ、「聞く」と「喋る」です。セッションを視聴しているとき、そして、セッション後に感想を伝えたり追加質問をしたり、ブース巡りをする際、ここだけはリアルタイム性がめちゃくちゃ必要で、スマホを取り出して翻訳している余裕なんてありません。去年はここに苦戦して、なんなら勇気が出ませんでした。
というわけで、「聞く」をテクノロジーで殴ることにして買ったのがRokid Glassesです。
※「喋る」は諦めて自力でやっていくしかない、というのが今時点の結論です。°(´ฅωฅ`)°。
この話は先日のLT会で発表させていただいたので、興味のある方はこちらもどうぞ。
そのLTで「開発にはケーブルが必要」と言ってしまいました
Rokid Glassesは標準機能でもリアルタイム翻訳ができるので、「聞く」問題はある程度これで解決します。でも、せっかく高い買い物をしたのであれば、リアルタイム翻訳機能だけで満足するのは勿体無いですよね。その先、re:Inventをもっと楽しく、もっと快適にすることができたらサイコーですよね!!!
ということで、以下のようなアプリの開発を計画しています。
- 「次のセッションどこだっけ?」とRokidに話しかける→ 目の前に会場と移動時間が出てくる
- What's Newを監視して、新機能がリリースされたら通知
- 新機能リリースに伴い、セッションが追加されたら通知
そんな妄想を膨らませてLTで話したのですが、そのとき私はスライドにこう書いていました。
アプリ開発(ただし、非売品ケーブルが必要)
開発ケーブルさえあれば、自作Rokidアプリから AgentCore を直接呼べる、、、らしい
はい。というわけで7月某日、無事に開発用ケーブルを入手し、意気揚々と開発を始めようとしました。
ただ、、、結論から言うと、このケーブル、一度も使いませんでした。
LTでの発言、訂正させていただきます。。。なぜ要らなかったのか?については後半で記載しておりますのでぜひご覧ください。
日本語情報がゼロという壁
ちなみに開発を始めて割とすぐ気づいたのですが、この領域、日本語の情報がびっくりするほど出てきません。
Makuakeでの先行販売開始が2026年2月26日、一番早めに利用開始した人で5月前後、一般販売が7月10日ということで、これからいろいろ情報が出てくる、、、といいなぁと切に願っています。(私が先駆者に!!!なれているといいですが)
なお、5月23日に「Rokid交流サロン」という、購入を検討している人と購入済みの人、販売会社の人の交流会があり、そこで先行入手した方々のLTを聞いたのですが、あまりエンジニアエンジニアした方は登壇されていないように感じました。(聴講者もエンジニアは少ないように感じました。これは主観です)
そういう背景もあるのかもしれません。
公式ドキュメントは中国語(英語版もあります)、しかも後述しますが検索エンジンにすら載らない作りになっていて、AIに読ませようとしても中身が取れないという状態でした。
そんな状況から手探りで進めて、最終的には自作アプリを実機のグラスに表示させるところまで到達できたので、その全手順と、道中で踏み抜いた罠を全部書き残しておきます。
本記事は3部作(今の想定ですが)の第1回です。
- 第1回(本記事)は、とにかく実機で動くものを1つ作るまで
- 第2回は、実際に作りたいアプリに向けた各種連携機能の検証(
wx.requestでのAPI呼び出し、SSEストリーミング、音声入力) - 第3回で、re:Invent向けアプリを実装します
第2回以降ではAWS(Bedrock AgentCore)と繋いでいく予定です。
この記事で伝えたいこと
実機で動かすまでの全手順を書きますが、道中で詰まったところを先に並べておきます。ここがそのまま、この記事で伝えたいことでもあります。手を動かしながら読む方は、目次代わりに使ってください。
| # | 罠 | どの工程で踏むか |
|---|---|---|
| ① | 開発ルートが2つあり、最初に混同する | 開発を始める前 |
| ② | 公式ドキュメントが検索にもAIにも読めない | 調べはじめ |
| ③ | (not 本質)ユーザー名が通らない(理由を教えてくれない) | アカウント作成 |
| ④ | Craftに日本語UIがあることに気づかない | Craftを開いたとき |
| ⑤ | 「エージェントを実行」がグレーアウトして押せない | Craftでプレビュー |
| ⑥ | 公式ドキュメント記載のCLIが存在しない | パッケージ化 |
| ⑦ | 「送信」ボタンを押してはいけない | アップロード後 |
| ⑧ |
ink:if の中で ink:for を使うと描画が壊れる |
実装中(実機検証で発覚。記事の最後で扱います) |
そして記事の最後に、実機でしか観測できないテンプル(耳にかけるつるの部分)操作のキーイベント仕様を、公式ドキュメント未記載の情報を含めてまとめています。個人的にはここが本命なので、手順に興味がない方は「実機でしか分からなかったこと」まで飛ばしてもらっても大丈夫です。
Rokid Glassesとは
Rokid社が出しているARスマートグラスです。日本での正式な製品名は「Rokid スマートAIグラス」ですが、本記事では開発者向けの表記に合わせて「Rokid Glasses」で通します。
本体価格は109,890円(税込)で、ディスプレイ・カメラ・マイク・スピーカーを内蔵しています。スマートデバイスで有名な「OK xxxx」や「Hi xxxxx」のように、音声で「Hi Rokid、〜」と呼びかけて操作するデバイスとなっています。
標準機能だけでも以下のことができます。
- リアルタイム音声翻訳(ニアリアルタイムで文字起こし。ラグは1秒弱で、翻訳精度も悪くない印象)
- 見ているものを翻訳(カメラ経由)
- 道案内、写真・動画撮影
- テレプロンプター(登壇時に発表原稿を目の前に表示する)
- 他にもいろいろあるので、詳しくは後述の公式サイトをご覧ください
そして本記事の主題であるアプリ開発もできます。開発者目線で効いてくるのはこのあたりです。
- 画面表示は単色グリーンのみ(透過ディスプレイなので、黒は「現実が透ける」を意味する)
- キャンバスサイズは幅448px / 高さ120〜352px
- 入力はテンプル(つるの部分)のタッチパッドと音声
- 独自のアプリプラットフォーム「AIUI(AI User Interface)」を用意
公式スペックのディスプレイ解像度は640×480なので、上のキャンバスサイズと数字が合いません。
AIUIのデザインガイドが示す448pxは、アプリが描画に使える論理キャンバスの幅であって、パネルの物理解像度そのものではないようです。本記事で以降サイズの話が出てきたら、すべて論理キャンバス側の数字だと思ってください。
詳細は公式サイトをご覧ください。
手を動かす前に押さえておきたい前提
詰まりポイント①:開発ルートが2つあり、最初混同した
手順に入る前に、ここだけ先に整理させていただきます。最初の関門がこれでした。
Rokid Glassesでアプリを動かす方法は、実は大きく2ルートあります。
| ルートA: AIUIエージェント | ルートB: ネイティブAndroid | |
|---|---|---|
| 成果物 | .aix |
.apk |
| 書き方 | JavaScript + .ink(WeChatミニプログラム互換) |
Android / Kotlin |
| 配布方法 | Rokidクラウド経由 | ADBでサイドロード |
| 開発用ケーブル | 不要 | 必須 |
私もAndroid開発者ではないので、ここは調べながら書いています。
ネイティブAndroidというのは、AIUIという独自の枠を使わず、素のAndroidアプリとして作るということです。その成果物がAPKで、Androidアプリを1つにまとめた配布用ファイルのこと。Androidスマホに入っているアプリの実体がこれらしいです。ルートAの .aix と同じ立ち位置ですね。
そしてADB(Android Debug Bridge)は、PCからケーブル経由でAndroid端末を操作するコマンドラインツールで、アプリのインストールやログの取得ができます。サイドロードは、Google Playのようなストアを経由せず、手元から直接アプリを入れることです。
つまり「ADBでサイドロード」というルートBは、PCとグラスをケーブルで繋いで、自分で作ったアプリを直接押し込む、という意味になります。
Rokid Glassesの中身はAndroid OSなので、この方法でアプリを流し込むこともできます。これがルートBで、ここでは開発用ケーブルが必要になります。
ちなみにこのケーブル、普通のUSB-Cではなく5ピンの磁気吸着コネクタで、市販もされていません(ゆくゆくは市販される、という話を公式の方から聞きました。ただし時期は未定)。ではそれを使うと何ができるのか?という話は、記事の後半「で、結局あの開発用ケーブルは何のためにあるの?」でまとめて整理しています。
一方で、Rokidが公式に推している「AIUI(AI User Interface)」というアプリプラットフォームがルートAです。こちらはJavaScriptで書けて、配布はクラウド経由。ケーブルは1ミリも登場しません。どのくらい登場しないかというと、AIUIの公式ドキュメント一式を grep してみたところ、adb という文字列が一文字も存在しませんでした。
「アプリ開発をしよう!開発用ケーブル入手しなければ 〜 さあ、ケーブルが届いたぞ!繋いで開発しよう」というテンションでいると、数週間無駄にすることになりますなりました
今回私はAIUI(ルートA)で進めています。決め手はこのあたりでした。
- 音声起動・AIアシスタント連携といったグラスらしい機能が最初から使える
- JavaScriptで書けるのでまだ慣れ親しんでいる(とはいえアプリ開発者ではないので、Vibe Coding全開です)
- 配布がクラウド経由なので、物理的なセットアップが要らない
- そして何より開発用ケーブルの案内文に以下免責事項があって不安になった
・本ケーブルを使用してRokid Glassesのファームウェアを改変、書き込み、または更新する行為は、全てお客様ご自身の責任において行なってください。 ・これらの操作に起因して発生した機器の不具合、いわゆる「文鎮化(ブリック)」、その他いかなる損害についても、Rokidは一切の責任を負いかねます。
AIUIとは
Rokidのスマートグラス向けアプリプラットフォームです。「アプリ」ではなく「エージェント」という単位で作ります。
形態は2種類あって、画面全体を占有する「没入型AIUI(Scene Agent)」と、チャットの吹き出しの中にカード的に埋め込まれる「会話型AIUI(Cut / Slice)」に分かれます。今回作るのは前者の没入型です。
そしてファイル構成は「Open Agent Format(OAF)」という仕様に沿います。最小構成は以下の5ファイルです。
hello-glass/
├─ AGENTS.md # エージェントの人格・システム指示
├─ app.js # アプリ全体のライフサイクル
├─ app.json # ページ一覧・ウィンドウ設定
├─ package.json
└─ pages/index/index.ink # 実際のページ
app.json という構成からして、WeChatミニプログラム譲り、、、らしいですね。記法もかなり寄せて作成されています。
そして .ink というのが独自のシングルファイルコンポーネント形式で、Vueの .vue に近い構造をしています。
<script def> <!-- ページ設定(JSON) -->
<script setup> <!-- ロジック -->
<page> <!-- テンプレート -->
<style> <!-- スタイル -->
やってみる!
ここから実際の手順です。今回作るのは「ハローグラス」という、画面に一行メッセージを出してタップで切り替わるだけの単純なアプリです。
1. まず公式ドキュメントを手元に落とす
手を動かす前に、そもそも何をどうやることでアプリ開発ができるのか???を知らないと何も前に進めません。ただ、Rokid Glassesは世界的に見てもごく最近リリースされた製品のため、AIの学習データにもほとんど含まれていません。
そのため、まずは公式サイト、公式ドキュメント、公式マニュアルといった一次情報でAIの知識を補強しておくことが何より重要であると考え、まずこちらに着手しました。
詰まりポイント②:公式ドキュメントが検索にもAIにも読めない
AIUIの公式ドキュメントは https://js.rokid.com/AIUI/ にありました(Rokidの人に教えてもらいました)。ブラウザで開けば普通に読めます。
しかし、これが完全なクライアントサイドレンダリングのSPAでして。curl で叩くと1811バイトの空っぽのHTMLシェルしか返ってきません。
これが地味に面倒です。
- 検索エンジンにインデックスされない(だから日本語で検索しても何も出てこない)
- AIに「このURL読んで」と投げても中身が取れない
- ページ内検索も横断的にできない
そのため、AIに学習させるのが結構大変でした。
ドキュメントのソースはGitHubに全文公開されていた
探し回った結果、AIUI Developer Toolsの公式ドキュメントのソースがまるごとGitHubにありました。
Apache-2.0ライセンスで、documentation/ 配下が js.rokid.com/AIUI の中身そのものです。しかも zh-CN(中国語)と en-US(英語)の両方が揃っています。
git clone https://github.com/jsar-project/AIUI.git
ちなみに、AIに読ませるためのSkillsも同じリポジトリで公開されていました。skills/aiui-dev/SKILL.md がそれで、.ink のSFC仕様やAPIリファレンス、プロジェクト構成のガイドラインが1ファイルにまとまっています。
npx skills add https://github.com/jsar-project/AIUI/tree/main/skills/aiui-dev
開発の際は、GitHubをcloneするより先にこれを用意しておくほうが良いかもしれません。(sampleの作成が終わった後、本記事記載中に気がついたためまだ試せていませんが、後続アプリ開発の時は利用してみようと思います。)
2. Rokidアカウントを作る
開発を始めるにはRokidアカウントが必要です。以下から登録できます。
日本のメールアドレスだけで登録できます(中国の携帯番号は不要)。国/地域セレクタでもちゃんと「日本」が選べます。
詰まりポイント③:ユーザー名が通らない(エラーメッセージが何も教えてくれない)
これはアカウント作成&ログイン後の話になります。アカウント作成時にアカウント名の指定は不要なのですが、デフォルトで中国語のアカウント名が自動で振り分けられています。そのため、アルファベットに変えようとしたところ、以下エラーが発生しました。
userName is not allowed
いや、何がダメなのかわからぬ。
実際に切り分けた記録が以下です。
| 入力した値 | 結果 |
|---|---|
yuuuuuuu |
エラー |
yuuuuuuu1234 |
エラー(数字を足しても無駄) |
aaaaaaa |
エラー ← ここで判明 |
yu_dev |
通過 |
中国語しかダメなのか?とも思いましたが、公式ドキュメントが中国語だけでなく英語版も用意されていたことから、アルファベット利用禁止ではないだろうという仮定で進めています。
そして、aaaaaaa でもエラーになったので、特定の禁止ワードではなく「同一文字の連続」が原因と判断しました。
特段命名規則が定められたドキュメントがあるわけではなさそうですが、ユーザー名は「4〜16文字」「同一文字を連続させない」を守れば通るようです。(厳密には未検証)
同じところで詰まった方はこれで抜けられるはずなので、先を急ぐ方は次の手順に進んでください。以下は「なぜ弾かれたのか」が気になって深掘りした記録なので、興味のある方だけどうぞ。
なぜそうなるのか、実装から追ってみた
気になったのでフロントエンドのバンドルからエラーコード定義を探してみたところ、こうなっていました。
150028: userName is not allowed (中国語版: 用户名违规)
150029: headIcon is not allowed (中国語版: 头像违规)
「违规」は「規約違反」の意味です。そして注目すべきは、150020〜150025 がOAuth系で連番になっているのに対し、150028 と 150029 だけが後付けでペア追加されているように見えることです。
ここから先は実装を確認したわけではないので推測になりますが、ユーザー名とアバター画像をセットで審査するという組み合わせは、中国のクラウドベンダーが提供している「内容安全(コンテンツモデレーション)」サービスの典型的な適用範囲と一致します。だとすると、引っかかっていたのは禁止語フィルタではなく、スパム(灌水)判定ではないかと考えています。同一文字の反復はスパムの典型パターンなので、それなら aaaaaaa が弾かれたのも納得がいきます。
さらに調べていて分かったのですが、画面に表示される規則とサーバー側の実際の制限が食い違っていました。フロントには「2〜20文字」と表示されるのに、サーバー側(エラーコード 100034)は4〜16文字でした。
ちなみに実名認証は不要でした
Rokidの開発者登録まわりを見ていると「实名认证(実名認証)」という項目が出てきて、しかも説明文がなかなか強めでした。
区分は2つあって、「基础认证(基本認証)」は実名認証が必要なRokidアプリを使えるようになるもの。もう一方の「专业认证(専門家による認証)」は個人開発者および企業開発者向けで、こちらには「所开发服务归属企业所有(開発したサービスは企業に帰属する)」と書かれていて、うっかり身構えてしまいました。
しかし実際にやってみたところ、実名認証なしでAIUI Studioに入れて、エージェント作成から実機投入まで完走できました(2026年7月時点)。
認証のヒント文をよく読むと、中国《网络安全法》第24条を引いて「アプリの公開には実名認証が必要」と書いてあります。おそらく、Maybe、きっと、、、こういう制限があるのはストア公開であって、個人利用なら当面は回避できるのではないか?と想像しています。
なお証明書類の種別にはパスポート(护照)も選択肢としてあったので、日本から認証すること自体は可能そうでした。
3. 実機側の設定を確認する
スマホのRokid AIアプリから辿ります。
ホーム → デバイスカード右上の歯車 → グラス設定 → 開発者
3項目ありますが、意味はそれぞれこうです。
| 項目 | 用途 |
|---|---|
| グラスADBデバッグ | ルートB(ネイティブAPK)用。今回は使わない |
| AIUI →「グラス…を更新」 |
.aix を実機に降ろすボタン。今回の主役(実際に押すのは手順8)。… は、こちらが省略しているのではなく、ボタンの幅が足りずにアプリ側で文字が切れている状態です。実際の表示のまま載せています。 |
| 店頭デモモード | 触らない |
ついでに グラス設定 → 画面オフ時間 も見ておきましょう。デフォルトが20秒です。
実機で表示確認をしていると20秒でスッと消えて「あれ、落ちた?」と焦るので、伸ばすかオフにしておくのがおすすめです。
加えて、グラス設定 → 自動電源オフ設定 もオフにしておくと、アプリ実装が楽になると思いますよ(楽でした)。
そしてもう1箇所、カスタムインタラクション の画面にタッチパッドの操作仕様が載っています。これが後で効いてくるので見ておきましょう。
| 操作 | OS上の意味 |
|---|---|
| タップ | 確認 |
| ダブルタップ | 戻る |
| 長押し | AIアシスタント起動(OSが占有) |
| スワイプ | 順方向 |
長押しはOSに取られているので、アプリ側では使えません。
4. AIUI Studioでエージェントの「箱」を作る
コードを書く前に、クラウド側にエージェントの「箱」を作ります。
https://aiui-global.rokid.com/space にアクセスして Create です。
まだ何も作っていないので No AIUI agents yet. と表示されています。右上(または左メニュー)の Create から作成します。
Rokidのプラットフォームはいくつか入口があってややこしいのですが、整理するとこうなります。
| 名前 | URL | 役割 |
|---|---|---|
| AIUI Studio | aiui-global.rokid.com |
グローバル版。エージェントの管理 |
| Rizon(灵珠) | rizon.rokid.com |
中国版。同じくエージェント管理 |
| Craft | js.rokid.com/craft |
ブラウザIDE。コードを書いてパッケージ化する |
すべて同一のアカウント基盤(IdP: account-center.rokid.com)で繋がっています。
さて、Createフォームの入力内容はこんな感じです。
Agent Name ハローグラス
Agent Version 1.0.0
Category STUDY
Function Description 画面に一行のメッセージを表示するテスト用エージェント。
タップすると表示が切り替わり、ダブルタップで終了する。
Opening monologue ハローグラスを起動したよ。タップしてみてね。
* が付いている項目は全部必須です。Categoryは LIFE / WORK / ENTERTAINMENT / STUDY の4択で、Iconはデフォルトのまま進めました。
Prompt Testが地味にすごい
フォームの右側に Prompt Test という欄があって、説明文にこう書かれています。
Enter a phrase to test whether it can wake up the Agent.
Matching depends on the agent name, category, and function description on the left.
つまり、エージェントの音声呼び出しは「名前 + カテゴリ + 機能説明」でマッチングされていて、それを作成前にその場でテストできるということです。
試しに「ハローグラスを開いて」と入れてみたところ
Test Passed!!! 日本語のフレーズでちゃんと呼び出せることがこの時点で確認できました。
これ、地味に大きな発見でして。自分でASR(音声認識)を実装しなくてもアプリの音声起動ができるということなんですよね。楽ーーーー
作成直後のエラーは正常です
エージェントを作成すると自動でCraft画面に遷移するのですが、そこでエラーが出ました。
Cloud Project Import
Load failed. Please check the agent status or try again later.
The requested cloud project could not be found.
最初は「作成失敗した?」と焦りましたが、これは異常ではありません。まだ .aix というアプリファイルの本体を一度もアップロードしていないので、クラウド側に中身が存在しないだけです。箱自体はちゃんとできています。安心です。
5. ローカルにプロジェクトを作る
公式のスキャフォールドもあります。
npm create @yodaos-pkg/aiui-agent my-agent
2026年7月時点では、生成される template/package.json に scripts が入っておらず、README記載の npm start が動きません。手動で足す必要がありました。
実機の設計制約が独特なので先に把握しておく
コードを書く前に、デザインガイドを読んで把握しておくべき制約が結構ありました。
色まわりがいちばん独特です。
- 単色グリーンのみ。赤も青も物理的に出ないので、エラー表示を赤くできない
- プライマリカラーは
#40ff5e。透明度4段階(100 / 60 / 40 / 8%)で情報の階層を作る - 背景は純黒
#000000。透過ディスプレイなので、黒はそのまま「現実が透ける」を意味する
サイズと組み方の制約もあります。キャンバスは幅448px / 高さ120〜352px、フォントサイズの下限は実質16pxで、主要な指示は24〜36pxが目安です。そして flex-direction: column を明示しないと、実機では要素が横一列に並んでしまいます。
「エラーを赤くできない」というのは、緑文字しか表示できないRokid Glassesの仕様上当たり前なんですが、なんか不思議な感じがしますね。
年内にはカラー版のRokid Glassesがリリースされる予定という話を5月23日の会でRokid販売会社の方がおっしゃっていたので、ゆくゆくはエラーを赤字にできるかもしれません。
ただ、利用者曰く、緑の方が目に優しい=チカチカしないことや、現実世界と重ならない色であることから、意外と緑単色の方がいいという人は居続けるかもしれませんね。
作成したコード
コード一式は以下のリポジトリに置いてあるので、全文を見たい方はこちらをご覧ください。
今回作った pages/index/index.ink には、単に文字を出すだけでなく実機の入力仕様を観測する機能を持たせています(後述する「実機でしか分からなかったこと」のセクションで利用します)。
画面イメージはこんな感じです。
ハローグラス b3 ← ビルド識別子
タップ届いたよ ← 32px、緑。直前の操作に応じて切り替わる
tap 1 ← Enterの発火回数
E1 B0 U1 D1 L1 R1 G3 ← キー別の累積カウント
#8 ArrowDown +27ms ← 連番 + 前イベントからの経過ms
#7 ArrowRight +270ms
#6 GlobalHook +17701ms
#5 ArrowUp +39ms
累積カウントの G3 は「タップ1回 + スワイプ2回」の合計です。GlobalHook があらゆる操作の先頭に必ず来ることが、この数字だけでも読み取れます。
ロジックの中核はこの部分です。前述の <script setup> ブロックの中に書いています。2段階の終了確認を実装しています。
onKeyUp(event) {
const code = (event && event.code) || '';
if (code === 'Backspace') {
if (this.data.exitConfirm) {
// 2回目: preventDefaultしない → ホストがアプリを閉じる
return;
}
// 1回目: 終了を止めて確認画面を出す
if (event && event.preventDefault) {
event.preventDefault();
}
this.setData({ exitConfirm: true, showMain: false });
return;
}
if (code === 'Enter') {
if (this.data.exitConfirm) {
this.setData({ exitConfirm: false, showMain: true });
return;
}
this.advanceMessage();
}
}
短いコードですが、ここには公式リファレンス(framework/open-agent-format/page/events)を読まないと分からない仕様が3つ効いています。
その1:event.key ではなく event.code を使う
ブラウザのキーイベントには key と code の2つがあります。key はKeyboardEvent仕様が定めた値(文字だけでなく Enter や ArrowUp のような非文字キーも含む)、code は押された物理キーの位置を表す識別子です。
紛らわしいのですが、Enter / Backspace / ArrowUp あたりは key でも code でも同じ文字列になります。ここだけ見ていると、どちらを使っても変わらないように見えます。
差が出るのが GlobalHook(テンプルのタッチ)です。公式ドキュメントいわく「標準的なWebのキー値ではなく、デバイスが提供する追加の入力シグナル」で、そもそもKeyboardEvent仕様に存在しない独自の値です。AIUIはこれを code 側に独自値として乗せているので、event.code を見る必要があります。
その2:キーには「ホスト側のデフォルト動作」が最初から割り当てられている
ここが一番大事なところでした。アプリが何もしなくても、キーを離した時点でホスト(グラス本体のOS側)が勝手に動きます。公式ドキュメントに書かれているデフォルト動作がこちらです。
| キー | ホストのデフォルト動作 |
|---|---|
Backspace |
1階層戻る。戻り先が無ければアプリを閉じることをリクエストする |
ArrowUp / ArrowDown
|
ルートビューをスクロールする |
Enter |
ナビゲーションモードに入る、または現在のターゲットを有効化する |
つまり Backspace を放っておくと、アプリは勝手に閉じます。「終了する?」の確認画面を出す暇もありません。
その3:それを止めるのは onKeyUp の preventDefault()
デフォルト動作を引き取りたい場合は event.preventDefault() を呼びます。呼べばホストは何もしなくなり、以降の処理はページ側の責任になります。
ポイントは、これを onKeyUp で呼ぶことです。公式ドキュメントは onKeyDown について「即時通知の性質が強いコールバック」「通常は入力そのものを監視するために使用される」と書いていて、デフォルト動作の制御は onKeyUp 側に紐づいています。押した瞬間ではなく、離した時点で「この操作をどう扱うか」が決まる、という設計思想になっています。
この仕組みが分かると、冒頭のコードがなぜあの形なのかが見えてきます。
- 1回目の
Backspace:preventDefault()を呼んで終了を止め、確認画面を出す - 2回目の
Backspace: 何もせずにreturnする → ホストのデフォルト動作が走ってアプリが閉じる
「閉じる処理」を自前で書くのではなく、止めるのをやめるだけで閉じてくれる、というのがポイントです。
コミュニティの記事では wx.exitMiniProgram の後に遅延させて wx.navigateBack を呼ぶ、というハック的な終了処理が紹介されていたのですが、この仕組みを使えばそういった小細工は要りませんでした。
なお、キーイベント以外に onVoiceWakeup という音声ウェイクアップ用のコールバックも用意されています。今回は使っていませんが、event.keyword でマッチしたウェイクワードが取れるようです(デフォルトは leqi)。
テンプレートとスタイルの書き味
<page> ブロックはこんな感じです。HTMLというよりWeChatミニプログラムの wxml のスタイルです。div ではなく view、テキストは text で囲みます。
<page>
<view class="screen">
<view class="panel" ink:if="{{ showMain }}">
<text class="title">ハローグラス {{ build }}</text>
<text class="message">{{ message }}</text>
<text class="meta">tap {{ tapCount }}</text>
</view>
<view class="panel" ink:if="{{ exitConfirm }}">
<text class="message">終了する?</text>
<text class="hint">もう一度ダブルタップで終了</text>
</view>
</view>
</page>
条件分岐は ink:if、データバインドは {{ }} です。この ink:if が面倒なバグを引き起こすのですが、それはまた後ほど。
スタイルはCSSがだいたいそのまま使えます。ただし前述の制約が効いてくるので、flex-direction: column の明示と緑の透明度による階層づけがポイントになります。
.screen {
display: flex;
flex-direction: column; /* これが無いと実機で横一列になる */
width: 448px;
background: #000000; /* 純黒 = 現実が透ける */
}
.message {
font-size: 32px;
color: #40ff5e; /* 100%: 主役の情報 */
}
.meta {
font-size: 16px;
color: rgba(64, 255, 94, 0.6); /* 60%: 補助的な情報 */
}
色は緑1色しか出せないので、情報の優先度は「文字サイズ」と「透明度」だけで表現することになります。この割り切りが最初はもどかしいのですが、慣れると逆に悩むことが無くなり設計しやすいです。
6. Craftにインポートしてプレビューする
https://js.rokid.com/craft がブラウザIDEです。
左上のインポートボタンから3つの選択肢が出てきます。
- Local Folder(今回使うのはこれ)
- Local .aix(既存の
.aixを開く。誰かが作成したアプリを、パッケージ済みの状態で受け取ってそのまま投入する、、、というイメージです) - GitHub Subdirectory(前述で公開したGitHubリンクを連携できるかも? 未検証です)
詰まりポイント④:Craftには日本語UIがあります
これは早く気づきたかったのですが、上部ツールバーの言語アイコンから日本語に切り替えられます。
ツールバーもファイルツリーもチャット欄も日本語になるので、体感がかなり変わります。
ただし訳は中国語からの直訳っぽいクセや、翻訳漏れが多々ありました。手順7で出てくるプロジェクト切り替えのドロップダウンなどは、日本語に切り替えても見出しが中国語のまま残っています。日本人開発者が増えることで、ドキュメントや開発サイトの翻訳精度も上がることを期待しています!!!
詰まりポイント⑤:「エージェントを実行」が押せない(詰まりというよりTIPS)
フォルダをインポートしたのに、Run Agent / エージェントを実行 がドロップダウンごとグレーアウトして押せないという状態にハマりました。エラーメッセージも何も出ないので、パッと原因が分かりませんでした。
手がかりを探して 歯車(設定)→ プロジェクト → ローカルディレクトリ を開いてみたところ、原因がわかりました。
現在のプロジェクトソース
agents
これです。agents/hello-glass を選ぶべきところで、親の agents を選んでいました。どうやらルート直下に app.json が無いと、AIUIプロジェクトとして認識されないようです。
というわけで、app.json があるフォルダそのもの(hello-glass)を指定して、もう一度インポートし直しました。
はい、問題なくアクティブになりました。左のファイルツリーのルートが agents ではなく hello-glass に変わっていること、そして エージェントを実行 の右の ∨ から実行設定が開けるようになったことが確認できます。
インポートするフォルダの階層を間違えていただけ、という話でした。。。ただ、エラーも警告も一切出ないので、設定画面を開いて「現在のプロジェクトソース」を見るまで気づきませんでした。同じ症状に当たったら、まずここを見るようにしましょう。
なお、この設定はブラウザのローカルストレージにのみ保存されます。画面下部にも「これらの設定は現在のブラウザにのみ保存されます」と明記されています。
ブラウザを変えたりストレージをクリアするとディレクトリの指定が飛ぶので注意しましょう。
プレビューが便利
エージェントを実行 を押すと、Interactive InkViewというプレビューが立ち上がります。これが想像以上に使えました。
右側のパネルが 操作 / ログ / ネットワーク / カメラ / デバイス のタブを持っていて、操作パネルには <(Backspace)/ カーソル(Enter)/ ↑(ArrowUp)/ ↓(ArrowDown)の4ボタンがあります。「上記の操作は PC キーボードからも実行できます」とも書いてありました。
実機に持っていく前に、ここまで検証できてしまいました。
| 検証項目 | 結果 |
|---|---|
| 表示 | 緑・縦並び・透明度の階層すべて意図通り |
| タップ |
Enter が届く |
| 上下ボタン |
ArrowUp / ArrowDown が届く |
| 2段階終了 | 確認画面 → 2回目で終了リクエスト発火 |
| ライフサイクル |
onLaunch / onShow / onLoad が発火 |
システムログが終了ロジックの正しさを証明してくれた
そして、システムログのタブを開いていたら面白いものが流れてきました。
[packages/ink/src/instance.rs:1662]
Instance a60db7ea-... close request denied by host: RootBackspace
InkView が終了リクエストを検知し、遮断しました(source: root-backspace)。
これ、めちゃくちゃ良い情報でして。preventDefault() を呼ばなかったことでホストへ終了リクエストがちゃんと飛んでいて、プレビュー環境がそれを意図的に遮断している、ということが分かります。つまり実機なら閉じる、という裏付けが取れました。
先ほど「preventDefault() を呼ばなければホストが閉じてくれるはず」と書きましたが、それが机上の話ではなく実際にリクエストが飛んでいる、とログが裏付けてくれた形です。
7. パッケージ化して .aix を作る
プレビューで動くことは確認できたので、実機に持っていくための .aix を作ります。
詰まりポイント⑥:公式ドキュメント記載のCLIが存在しない
まずは公式ドキュメントの手順を追ってみます。.aix を作るためのCLIを、こうやってインストールしろと書いてあります。
cargo install --path packages/aix-cli
しかし、その packages/aix-cli は公開リポジトリに存在しません。パスを指定しろと言われても、指定すべきものが無い。。。
コミュニティのプロジェクトを覗いてみると aiui-open / aiui-aix という別名のバイナリを使っているようなのですが、セットアップスクリプトが ~/Downloads からzipを展開する作りになっていて、そもそもの入手元が書かれていません。
ただ、結論としてはCLIは要りませんでした。Craftの「パッケージ化」ボタンが全部やってくれます。
Craftでパッケージ化する
上部ツールバーの「パッケージ化」→ 設定確認ダイアログ →「パッケージ開始」です。
| 設定 | 既定値 | 対応するCLIオプション |
|---|---|---|
| アセット最適化 | 有効 | --optimize |
| JSON検証 | 有効 | 対応オプションなし |
| 最適化レベル | Level 2 | --opt-level |
設定項目にCLIのオプション名がそのまま並んでいるので、Craftは内部で同じCLIを呼んでいるのだと思われます。入手できなかったCLIの機能が、結局ボタン1つで使えたことになります。
.aix はクラウド保存が基本で、アップロード先のエージェントを選択する形になります。ローカルへのダウンロードも可能でした。
.aix の中身を覗いてみる
せっかくなので中身を見てみましょう。
unzip -l hello-glass-1.0.0.aix # ファイル一覧とサイズ
unzip -v hello-glass-1.0.0.aix # 圧縮方式まで見たいときはこちら
VERSION 36 bytes ← 自動生成されたUUID
AGENTS.md 1,012
app.js 297
app.json 88
package.json 278
pages/index/index.ink 3,251
──────────────────────────────
8 entries 4,962 bytes(ファイルサイズ 5.6K)
「8 entries」なのにファイルが6つしかないのは、pages/ と pages/index/ のディレクトリエントリが2つ含まれているためです。
分かったことは以下の2点です。
- 形式はZIPコンテナだが、圧縮方式は
store(無圧縮)。unzip -vのMethod列がすべてStoredになっていました -
VERSIONが36バイト = UUIDそのもの。公式ドキュメントの「ホットアップデート判定用に一意のUUIDを自動生成する」という記述がこれに当たります- バージョンを手動指定することもできましたが、「必要な場合のみカスタムを展開してください。」という記述がありました。
※カスタム=メジャー/マイナー/パッチバージョンを指定すること
- バージョンを手動指定することもできましたが、「必要な場合のみカスタムを展開してください。」という記述がありました。
詰まりポイント⑦:「送信」ボタンは押してはいけない
アップロード後、Craft左上のドロップダウンを開くとこうなっています。
本地项目(ローカルプロジェクト)
hello-glass 28 分前
云端项目(クラウドプロジェクト)
ハローグラス 1.0.1 ✓
- 本地項目 = 手元のフォルダを見ている状態
- 云端項目 = クラウドにアップ済みのエージェントを見ている状態(
VERSIONファイルが見えます)
ちなみにこちらは日本語UIに切り替えた状態のスクリーンショットです。ツールバーは「ドキュメント」「パッケージ化」「送信」「エージェントを実行」と全部日本語になっているのに、このドロップダウンの見出しだけ中国語のまま残っています。項目の中身(「28 分前」)は日本語なので、グループ見出しの翻訳が漏れているようです。
なお、ここで 1.0.1 と表示されているのを見て「あれ?」となりました。手順4のCreateフォームでは 1.0.0 と入力したはずなのに、値が変わっています。パッケージ化のたびにクラウド側が採番を進めているようで、手元のコードに書いたバージョンとは一致しません。これが後述の「ビルド識別子を自分で持つ」動機になります。
そして「送信」ボタンは云端項目を選択している間だけ有効になります。
この「送信」はAgent Store(一般公開ストア)への審査提出です。
実機に降ろすだけなら審査は一切不要なので、個人利用の場合は絶対に押さないようにしましょう。
私は最初「送信を押さないと実機に行かないのでは?」と思って押しかけたのですが、危ないですね。
8. 実機へ降ろす
いよいよです。スマホのRokid AIアプリから、
設定 → 開発者 → AIUI →「グラス…を更新」
これだけです。
そしてグラスを掛けて、「Hi Rokid、ハローグラスを開いて」と話しかけると……
動いたーーーー!!!
自分の書いたJavaScriptが目の前の空中に緑色で浮かんでいるの、いいですね。表示崩れもなく、縦並び・フォントサイズ・透明度の階層すべてが意図通りに出ています。
ダブルタップすると、実装した通り2段階の終了確認もちゃんと出てくれました。
ところで、ケーブル繋いでないんですけど?
さて、ここで冷静になると重大な疑問が湧いてきます。
PCとスマホも繋いでないし、PCとグラスも繋いでない。なのになぜPCで書いたコードがグラスに届いたのか?
最初は分からなかったのですが、答えはシンプルで、すべてクラウド経由だったからです。そして重要なのは、PCとスマホは互いに何も連携していないという点です。
PC (Craft) スマホ (Rokid AIアプリ)
│ │
yu_dev でログイン yu_dev でログイン
│ │
└────────► Rokidクラウド ◄────────┘
「yu_dev のハローグラス」
│
│ ペアリング済みの無線接続
▼
Rokid Glasses
PCがやったのは「yu_devというアカウントの、ハローグラスという箱に .aix を置く」だけです。スマホの方は「yu_devというアカウントに紐づくデバッグ用エージェントをちょうだい」とクラウドに問い合わせ、受け取ったものをグラスに流しています。
根拠としては、Craft / AIUI Studio / Rizonが読み込む認証基盤(IdP)が全部同一(account-center.rokid.com)であること、そしてCraftの文言が「現在のアカウントで利用可能な AIUI エージェントを取得し」となっていることです。
なお、スマホ→グラス間の転送に実際どのプロトコルが使われているかまでは追えていません。ペアリング自体はBluetoothですが、.aix の実体がBluetoothで流れているのかWi-Fiに切り替わっているのかは未検証です。
「ケーブルが要らない」理由
ここまで来て、ようやく腑に落ちました。
- ケーブル + ADB = 「ネイティブAPKを物理的に押し込む」ための経路
- AIUI = 「クラウド経由で配信される」ための仕組み
設計思想がまったく違うんですね。同じデバイスに対する開発なのに、経路が完全に分かれている。「開発するならケーブルでしょ」という思い込みが、そもそもルートBの発想だったという話でした。
というわけで、冒頭のLTで「開発ケーブルさえあれば自作アプリからAgentCoreを呼べる」と言っていた件、正確にはこうなります。
ルートA(.aix)ならケーブルは不要、ルートB(.apk)なら必須。LTで言っていたのは、後者だけの話でした。
そしてAgentCoreを呼ぶだけなら、ルートAで足りるはずです。HTTPリクエストを投げる wx.request がAIUIのAPIとして用意されているためですが、実機で本当に外部APIを叩けるかはまだ確認できていません。ここは第2回で検証します。
いずれにせよ、ケーブルが無くてもアプリを実機投入まではできる、というのが今回の一番大きなポイントです。
他の人が「ハローグラスを開いて」って言ったらどうなるの?
クラウド配信と聞いて次に心配になったのがこれです。世界中のRokidユーザーが「ハローグラスを開いて」と言ったら、私のアプリが起動しちゃうのでは?と。
結論から言うと、開きません。そもそも3段階でブロックされていました。
- そもそも他人の端末に
.aixが入っていない(コードの実体が無ければ起動しようがない) - Agent Storeに公開していない(「送信」= 審査提出を押していないので、他人が入手する経路が存在しない)
- 配信がアカウントスコープ(Craftもスマホアプリも、同一アカウントでログインした状態でのみ機能する)
中国語圏の記事でも「個人開発者が作ったカスタム智能体は審査なしで私有化呼び出しが可能」という趣旨の記述を見かけたので、この理解と大きくはズレていないはずです(一次情報ではないので、あくまで傍証として)。
実際にスマホアプリの「エージェントストア」で自分のエージェント名を検索してみても、当然ヒットしませんでした。安心ですね。
ただし、.aix 自体には何の保護もかかっていません
一方で、.aix の中身を見て気づいたのですが、中身はコードとJSONと VERSION ファイルだけです。難読化も暗号化もされておらず、署名ファイルの類も一切入っていません。ソースがそのまま読める状態です。
| 守られているもの | 何で守っているか |
|---|---|
| 誰がアップロードできるか | Rokidアカウントのログイン |
| 誰がダウンロードできるか | 同じアカウントでログインしたスマホのみ |
| 誰が実行できるか | そのスマホとペアリングされたグラス |
| パッケージの中身 | 何も守られていない |
守られているのは「配信経路」だけで、「パッケージそのもの」は素通しでした。
なお、改変した .aix を実機が受け付けるかどうか(=ランタイム側に検証があるか)までは試していません。ここで言えるのは「パッケージの中身が誰でも読める」ところまでです。
これは第2回以降において重要になります。
Bedrockなどの外部APIに繋ぐとき、.ink にAPIキーをベタ書きすると .aix を入手した人に全部見えます。「実行時にトークンを取得する」設計(Cognito JWTなど)が必須になりそうです。
で、結局あの開発用ケーブルは何のためにあるの?
「要らなかった」で終わらせるとせっかく頂戴したのに申し訳ないので、本来なにができるものなのかも整理しておきます。
前述の通り5ピンの磁気吸着コネクタで、市販もされていません。そこまでして繋いだうえでできることは、大きく2つです。
- APKのサイドロード(
adb install xxx.apk)。AIUIの枠を出て、Androidアプリとして書けるようになります - ログの取得(
adb logcat)。実機の中で何が起きているかが見えます
有効化の手順はこんな流れになります。
- PCに Android SDK Platform Tools(
adbコマンドが入っている公式パッケージ)を入れる - スマホのRokid AIアプリでグラスに接続する
-
歯車 → グラス設定 → 開発者 → グラスADBデバッグをONにする(手順3で「今回は使わない」と言っていたトグルがこれです) - 開発用ケーブルでPCに接続し、
adb devicesで認識されることを確認する adb install xxx.apk
この手順はRokidの公式ドキュメントではなく、コミュニティの検証情報がもとになっています。
前述の通り、AIUI公式ドキュメントには adb の記述が一切存在しません。
AIUI開発でも1つだけ出番がありそう
今回はまったく使いませんでしたが、adb logcat だけは将来お世話になる可能性があると思っています。
というのも、手順6のCraftプレビューで見られたログは、あくまでInkランタイムがアプリに見せてくれている範囲のものです。実機が黙って落ちるような、ランタイムより下のレイヤーで起きる問題に当たったとき、外から中を覗く手段が他にありません。
なので「AIUIしかやらないからケーブルは不要」ではなく、保険としてADBが繋がる状態にはしておく、というのが実際のところかなと思います。
ネイティブにしても解けない制約もある
「ルートBに行けば何でもできる」わけではない点だけ補足しておきます。
| 制約 | ルートBで解ける? |
|---|---|
| ランチャーに載らない(音声起動のみ) | 解ける可能性あり(Androidアプリとして扱われるため) |
| 表示が単色グリーンのみ | 解けない |
| キャンバスが幅448px | 解けない |
下2つはディスプレイそのものの物理特性なので、どんな書き方をしても変わりません。緑1色・448pxという縛りは、ルートを変えても付いてきます。
というわけで、箱に入ったままのケーブルにも一応の出番はありそうです。よかった。
実機でしか分からなかったこと
ここからが本記事のいちばんの目玉です。公式ドキュメントに載っていない、実機がないと絶対に書けない情報を書きます。
公式ドキュメントに無いキーコードが飛んでくる
公式リファレンスが列挙しているキーコード(テンプルをどう操作したかを、OSがアプリに通知するためのキーワード)は5つです。
Enter / Backspace / ArrowUp / ArrowDown / GlobalHook
しかし実機で観測したところ、ArrowLeft と ArrowRight も飛んできました。公式ドキュメントには一切記載がありません。
テンプル操作 → キーイベント完全マップ(実測)
各操作の間を3秒以上空けながら、連番と経過msを記録して整理したのが以下です。
| 操作 | イベント列 | 間隔(実測レンジ) |
|---|---|---|
| シングルタップ |
GlobalHook → Enter
|
333〜494ms |
| ダブルタップ |
GlobalHook → GlobalHook → Backspace
|
228ms → 278ms(※1回のみ測定) |
| 前→後ろスワイプ |
GlobalHook → ArrowLeft → ArrowUp
|
143〜302ms → 22〜39ms |
| 後ろ→前スワイプ |
GlobalHook → ArrowRight → ArrowDown
|
205〜270ms → 25〜27ms |
実際の実機写真がこちらです。
シングルタップ:#1 GlobalHook +0ms → #2 Enter +333ms
前→後ろスワイプ:#4 ArrowLeft +302ms → #5 ArrowUp +39ms
後ろ→前スワイプ:#7 ArrowRight +270ms → #8 ArrowDown +27ms
間隔は固定値ではありません。
2回に分けて測定したところ、イベントの種類と個数は完全に再現したのですが、間隔にばらつきがありました。
区間ごとに性質が違うのが面白いところです。
| 区間 | レンジ | 解釈 |
|---|---|---|
GlobalHook → Enter
|
333〜494ms | 「指が触れている時間」+「ダブルタップ判定の待機時間」の合算と思われる(詳細は発見A) |
GlobalHook → 水平キー |
143〜302ms | スワイプ動作そのものにかかった実時間。指を動かす速さで変わる |
| 水平キー → 垂直キー | 22〜39ms | ここだけ安定して短い = 水平と垂直はセットで吐かれている |
判明した構造
観測結果から、以下の構造が見えてきました。
-
GlobalHookはあらゆる操作の先頭に必ず来る。「テンプルに触れた」という生のシグナルで、この時点ではまだ操作の種類(タップ / ダブルタップ / スワイプ)が確定していない -
Enter/Backspace/Arrow*は、OSが操作を解釈し終えた結果として後から届く
これは公式ドキュメントの「GlobalHook は標準的なWebのキー値ではなく、デバイスが提供する追加の入力シグナル」という説明と整合します。
発見A:シングルタップの反応は0.3〜0.5秒遅れる
GlobalHook → Enter の間隔が333ms / 480ms / 494msでした(3回の実測)。
主因はダブルタップ判定の待機時間だと考えています。実際にダブルタップの2打目は228ms後に来ていました。OSは「一定時間待って2打目が来なければシングルタップと確定」する作りになっているのでしょう。
ただ、それだけだと待機時間は固定値になるはずで、実測が160msもばらついている説明がつきません。ここで効いてくるのが GlobalHook の位置です。GlobalHook は「テンプルに触れた」瞬間に飛ぶので、そこから Enter までの間隔は
GlobalHook → Enter = 指が触れている時間 + ダブルタップ判定の待機時間
の合算になります。前半は自分の指の速さ次第なので、ばらつくのは当然でした。
つまり Enter を待つUIは、待機時間のぶんだけ確実に遅れて、体感で0.3〜0.5秒もたつきます。
即応させたいなら GlobalHook を使う手もありますが、スワイプでもダブルタップでも飛んでくるので確実に誤爆します。UXとしてはトレードオフですね。
発見B:1スワイプで方向キーが2個飛ぶ(重大な罠)
これがいちばん危ないやつです。スワイプは水平キーと垂直キーの両方を発火します。
// ❌ これをやると、1回のスワイプでリストが2つ進んでしまう
if (code === 'ArrowUp' || code === 'ArrowLeft') {
this.prev();
}
どちらか一方だけを見る必要があります。
どちらを採るかなら、公式ドキュメントに記載のある ArrowUp / ArrowDown をおすすめします。
実測では、前→後ろが ArrowLeft かつ ArrowUp、後ろ→前が ArrowRight かつ ArrowDown で固定でした。スワイプ方向とキーが1対1で対応するのは水平・垂直どちらも同じなので、そこは判断材料になりません。
むしろ最初は ArrowLeft / ArrowRight のほうが良さそうに見えていました。テンプルをなぞる動きは物理的に水平ですし、ArrowUp / ArrowDown にはホスト側のデフォルト動作(ルートビューのスクロール)があるぶん、面倒に思えたからです。
ところがこれ、よく考えると成り立ちません。1スワイプで水平キーと垂直キーの両方が飛ぶので、ArrowLeft で処理を書いたところで ArrowUp はホストに届いてしまいます。放っておけばルートビューが勝手にスクロールするので、結局 preventDefault() は必要でした。
書く量で比べるとこうなります。
| 採用するキー | 書くこと | 触るキー |
|---|---|---|
ArrowUp / ArrowDown
|
Up/DownをpreventDefault()して、そこで処理する |
2つ |
ArrowLeft / ArrowRight
|
Left/Rightで処理する + Up/DownをpreventDefault()で握り潰す |
4つ |
素直なのは ArrowUp / ArrowDown のほうでした。
さらに実務的な理由がもう1つあります。前述したCraftプレビューの操作パネル、よく見るとボタンは < / カーソル / ↑ / ↓ の4つだけで、左右のボタンがありません。ArrowLeft / ArrowRight に依存すると、リスト送りの動作確認をするたびにパッケージ化して実機へ降ろす羽目になります。せっかく優秀なプレビューがあるのに、自分から捨てにいくことになってしまいますね。
そして何より、ArrowLeft / ArrowRight はそもそも公式ドキュメントに存在しないキーです。今は飛んできますが、将来も飛んでくる保証はありません。
というわけで、ArrowUp / ArrowDown を onKeyUp で preventDefault() しつつ処理に使い、ArrowLeft / ArrowRight は「存在を知っておくが使わない」が落としどころだと考えています。
1つ正直に書いておくと、ArrowUp に preventDefault() を呼べば実際にスクロールが止まるのかは、まだ実機で確認できていません。公式ドキュメントの記載を根拠にしているだけです。
同じく「ArrowLeft / ArrowRight にデフォルト動作が無い」も、記載が無いというだけで検証はしていません。ここは次回リストUIを作るときに確かめて、結果を追記します。
発見C:タップとスワイプは確実に区別できる
スワイプを何回繰り返しても、tap カウント(Enter の発火回数)は増えませんでした。
つまり決定に Enter、リスト送りに ArrowUp / ArrowDown を割り当てる設計が成立します。
設計上の落とし穴まとめ
| 罠 | 何が起きるか |
|---|---|
Enter と GlobalHook を両方見る |
1タップで2回処理が走る |
ArrowUp/Down と ArrowLeft/Right を両方見る |
1スワイプで2回処理が走る |
Enter の即応性を期待する |
0.3〜0.5秒遅れる |
ArrowUp/Down を握り潰さない |
ホストのスクロールが自前の処理に重なる。ArrowLeft/Right を使う設計にしても飛んでくるので、握り潰しは必須 |
観測機の作り方(この設計自体がノウハウでした)
ちなみに、この調査は最初うまくいきませんでした。届いたキーコードの履歴を並べるだけだと、前の操作と混ざってどこが切れ目か分からないんですね。
そこで次の3点を画面に出すようにしたら、一気に解決しました。
ハローグラス b3 ← (1) ビルド識別子
タップ届いたよ
tap 1
E1 B0 U0 D0 L0 R0 G1 ← (2) キー別の累積カウント
#2 Enter +480ms ← (3) 連番 + 前イベントからの経過ms
#1 GlobalHook +0ms
累積カウントを出しておくと、1操作でどのキーが何個増えたかが一目で分かります。経過msは、同一操作内なら数十〜数百msに収まり、操作の切れ目では秒単位で空くので、機械的に分離できます。ビルド識別子は、実機に届いているのが新版か旧版かを目視で確定するためのものです。
特に経過msが調査に役立ちました。これのおかげで「この3イベントは1回のスワイプで飛んだもの」と断定できるようになっています。
観測のコツは各操作の間を3秒以上空けることです。ついでに、起動直後にカウントが全部0であることも確認できるので、起動時にはキーイベントが飛ばないという事実も分かりました。
ビルド識別子を画面に出すのは強くおすすめ
手順7で見たとおり、クラウド側のバージョン表示(1.0.0 → 1.0.1)はパッケージ化のたびに勝手に進んでいくので、手元のコードとの対応が当てになりません。そこでコードに独自の識別子を持たせて画面の隅に出しておきます。
const BUILD = 'b3'; // コードを変えたら手で上げる
<text class="title">ハローグラス {{ build }}</text>
実機を見た瞬間に「更新が反映されたか」が判別できます。コミュニティのフィールドノート(saibozhanzhang/rokid-aiui-lab)にある「古いパッケージが入ったままなのに気づかず延々デバッグする」という罠への、直接的な対策になりました。
詰まりポイント⑧:ink:if の中で ink:for を使うと描画が壊れる
最後に、実害のあったバグを1つ共有します。実機とCraftプレビューの両方で再現しました。
症状
もともとログの表示は ink:for でループさせていました。
<view class="panel" ink:if="{{ showMain }}">
<text class="title">ハローグラス {{ build }}</text>
<text class="message">{{ message }}</text>
<text class="meta">tap {{ tapCount }}</text> <!-- 消える -->
<text class="counts">{{ counts }}</text> <!-- 消える -->
<text class="log" ink:for="{{ logs }}" ink:key="*this">{{ item }}</text> <!-- 消える -->
</view>
これで以下の手順を踏むと、表示が壊れます。
- 起動直後は全部正常に表示される
- ダブルタップで終了確認画面へ(
showMain: false) - タップでキャンセルして戻る(
showMain: true) -
messageは復活するが、tap N/ カウント行 / ログ行が消えたまま戻らない
this.data の値自体は保持されています。描画だけが復元されません。
ink:for をやめて固定の文字列バインドにしたら直った
next.log0 = logs[0] || '';
next.log1 = logs[1] || '';
next.log2 = logs[2] || '';
next.log3 = logs[3] || '';
<text class="log">{{ log0 }}</text>
<text class="log">{{ log1 }}</text>
<text class="log">{{ log2 }}</text>
<text class="log">{{ log3 }}</text>
ink:for を1個消しただけで、ブロック全体の描画が正常に戻りました。
コミュニティのフィールドノートに「ページのコードは単純なほど安定する」「凝ったテンプレート記法は実機で不安定」という記載があったのですが、その具体的な裏付け事例になりました。教訓としては、ink:if で出し入れするブロックの中には、凝った記法を置かないのが安全そうです。
ランチャーには載らなかった
最後にもう1つ、想定外だったことを。
このアプリ、アイコン一覧から選んで起動することはできませんでした。音声で呼ぶ必要があります。
グラス本体、Rokid AIスマホアプリ、AIUI Studioの3箇所を確認しましたが、開発者モードで入れたエージェントはどこにも現れませんでした。
ただこれは仕様と整合しています。没入型AIUI(Scene Agent)は公式ドキュメントで「ページを個別のツールとして定義せず、エージェント自身のdescriptionを通じてエージェント全体を直接呼び出す」と定義されているので、そもそも音声呼び出しが基本設計なのでした。
推測ですが、Agent Storeに公開したものだけがランチャーに載るのではないかと思っています(未確認)。個人利用でストア公開をしたくない場合は、音声起動のみという割り切りになりそうです。
実用上の対策としては、エージェント名を短く(2〜4音)して呼びやすくするのが良さそうです。「ハローグラス」は正直ちょっと長かった。。。
そもそもグラスは手がふさがった状態で使うデバイスなので、ランチャーを目で探すより「呼ぶ」ほうが速いという捉え方もできますね。
おわりに
というわけで、Rokid Glassesに自作アプリを実機投入するまでの全記録でした。
正直に言うと、技術的な難易度そのものは全然高くありませんでした。書いたのはJavaScriptとHTMLライクなテンプレートだけですし、コード量も200行程度です。
大変だったのは全部情報にたどり着くまでの部分でした。ドキュメントがSPAで読めない、CLIが存在しない、ユーザー名が通らない、ボタンがグレーアウトしている理由が分からない……。手を動かす時間より、調べて切り分けている時間のほうが圧倒的に長かったです。
でも、こういう「誰も日本語で書いていない領域」を自分で切り拓いていく作業、めちゃくちゃ楽しいですね!!! 特に実機で緑の文字が浮かんだ瞬間と、キーイベントの構造が GlobalHook 起点だと分かった瞬間は、思わず声が出ました。
そして「開発ケーブルが必要」という前提が崩れたのは、地味に大きな前進でした。ケーブルは非売品でそもそも入手のハードルが高いので、それ無しでAWS連携まで行けるなら、同じことをやりたい人にとって参入障壁がかなり下がるはずです。
次回(第2回)は、いよいよ外部との通信に踏み込んでいきます。
-
wx.requestで固定のAPIを叩いて結果を表示する - AWS Bedrock AgentCoreに繋いで、レスポンスをSSEでストリーミング表示する
- 音声入力(ASR)が実機でどこまで使えるか
特にSSEストリーミングは onChunkReceived を使う想定なのですが、この組み合わせの前例が世界的に見つかりません。動くのかどうか、現時点ではまったく分かりません。動いても動かなくても面白い記事になると思うので、お楽しみに!
ここが通れば、冒頭に書いた妄想(「次のセッションどこ?」と聞いたら目の前に出てくるコンシェルジュ、What's Newの日本語要約が視界に流れてくるウォッチャー)に一気に近づきます。re:Invent 2026までに間に合わせたい!!
同じようにRokid Glassesを買ったはいいものの開発の入口が分からず困っている方の、最初の一歩の助けになれば嬉しいです。
参考
- yuuuuuuu168/rokid-hello-glass - 本記事で作ったサンプルアプリのコード一式
- re:Invent2025で英語に苦戦したのでテクノロジーで解決しようとしている話 - 本記事の発端となったLT資料。Even G2との比較や入国審査での注意点も
- Rokid Glasses 公式サイト(日本) - 製品情報・購入先
- AIUI 公式ドキュメント - Rokidのアプリプラットフォーム公式ドキュメント。ブラウザでのみ読める
- jsar-project/AIUI - 上記ドキュメントのソースが全文公開されているリポジトリ。zh-CN / en-US 両方あり
- Craft - ブラウザで動くAIUI開発IDE。パッケージ化まで可能
- AIUI Studio - グローバル版のエージェント管理コンソール
- Rizon(灵珠) - 中国版のエージェント管理コンソール
- Rokid アカウント登録 - 日本のメールアドレスで登録可能
- saibozhanzhang/rokid-aiui-lab - コミュニティによる実機フィールドノート。実機での不安定な挙動の情報が豊富
- Anezium/awesome-rokid - Rokid関連リソースのまとめ
- Rokid Glasses 開発者模式與 ADB 側載 - ルートB(ネイティブAPKのサイドロード)の手順。繁体字



















