1. 始める前に
スマートホームの統合により、ユーザーの家にある接続済みデバイスを Google アシスタントを通じて制御できるようになります。Cloud 間インテグレーションを構築するには、スマートホーム インテントを処理できるクラウド Webhook エンドポイントを用意する必要があります。たとえば、ユーザーが「OK Google, 電気をつけて」と言うと、アシスタントはクラウド フルフィルメントにコマンドを送信してデバイスの状態を更新します。
一方、Local Home SDK を使用すると、スマートホーム インテントを Google Home デバイスに直接ルーティングするローカルパスを追加できます。これによりスマートホームの統合を強化でき、ユーザー コマンドの処理の信頼性を向上させレイテンシを短縮できます。また、デバイスを識別するローカル フルフィルメント アプリを TypeScript や JavaScript で記述してデプロイし、Google Home スマート スピーカーや Google Nest スマートディスプレイでコマンドを実行することもできます。ユーザー コマンドの実行に既存の標準プロトコルを使用することで、アプリがローカルエリア ネットワーク経由で既存のスマート デバイスと直接通信することが可能になります。
前提条件
- クラウド間インテグレーションを作成するデベロッパー ガイド
- スマートホーム洗濯機の Codelab
- ローカル フルフィルメントのデベロッパー ガイド
作成するアプリの概要
この Codelab では、これまでに構築したスマートホーム統合を Firebase にデプロイし、デベロッパー コンソールでスキャン設定を適用します。さらに、TypeScript でローカルアプリをビルドし、Node.js で記述したコマンドを仮想の洗濯機デバイスに送信します。
学習内容
- デベロッパー コンソールでローカル フルフィルメントを有効にして設定する
- Local Home SDK を使用してローカル フルフィルメント アプリを記述する
- Google Home スピーカーまたは Google Nest スマートディスプレイに読み込まれたローカル フルフィルメント アプリをデバッグする
必要なもの
- 最新バージョンの Google Chrome
- Google Home アプリがインストールされている iOS または Android デバイス
- Google Home スマート スピーカーまたは Google Nest スマートディスプレイ
- Node.js バージョン 10.16 以降
- Google アカウント
- Google Cloud 請求先アカウント
2. はじめに
アクティビティ管理を有効にする
Google アシスタントを使用するには、特定のアクティビティ データを Google と共有する必要があります。このデータは Google アシスタントが適切に機能するために必要となります。ただし、データの共有は SDK に固有の要件ではありません。データを共有するには Google アカウントを作成します(まだ作成していない場合)。どの Google アカウントでも使用できます。デベロッパー アカウントである必要はありません。
アシスタントで使用する Google アカウントのアクティビティ管理ページを開きます。
以下の切り替えスイッチが有効になっていることを確認します。
- [ウェブとアプリのアクティビティ] - これに加えて、[Chrome の履歴と Google サービスを使用するサイト、アプリ、デバイスでのアクティビティを含める] チェックボックスがオンになっていることも確認してください。
- [デバイス情報]
- [音声アクティビティ]
Cloud 間インテグレーション プロジェクトを作成する
- Developer Console に移動します。
- [プロジェクトを作成] をクリックし、プロジェクトの名前を入力して [プロジェクトを作成] をクリックします。
クラウド間インテグレーションを選択する
デベロッパー コンソールの [プロジェクトのホーム] で、[クラウド間] の [クラウド間統合を追加] を選択します。
Firebase CLI をインストールする
Firebase コマンドライン インターフェース(CLI)を使用すると、ウェブアプリをローカルで提供し Firebase Hosting にデプロイできます。
CLI をインストールするには、ターミナルから次の npm コマンドを実行します。
npm install -g firebase-tools
CLI が正しくインストールされたことを確認するには、次のコマンドを実行します。
firebase --version
Google アカウントで Firebase CLI を承認するには、次のコマンドを実行します。
firebase login
HomeGraph API を有効にする
HomeGraph API を使用すると、ユーザーのホームグラフ内のデバイスとその状態を保存して照会できます。この API を使用するには、まず Google Cloud コンソールを開いて HomeGraph API を有効にする必要があります。
Google Cloud コンソールで、統合の <project-id>.
に一致するプロジェクトを選択し、HomeGraph API の [API ライブラリ] 画面で [有効にする] をクリックします。
3. スターター アプリを実行する
開発環境の設定が完了したので、スターター プロジェクトをデプロイし、すべてが正しく設定されていることを確認しましょう。
ソースコードを取得する
下のリンクをクリックして、この Codelab のサンプルを開発マシンにダウンロードします。
または、コマンドラインから GitHub リポジトリのクローンを作成することもできます。
git clone https://github.com/google-home/smarthome-local.git
プロジェクトについて
スターター プロジェクトには、以下のサブディレクトリが含まれています。
public
- スマート洗濯機を制御、監視するためのフロントエンド ウェブ UIfunctions
- Cloud-to-Cloud 統合用のクラウド フルフィルメントを実装する Cloud Functionslocal
-index.ts
にインテント ハンドラがスタブされたローカル フルフィルメント アプリのスケルトン プロジェクト
提供されるクラウド フルフィルメントの index.js
には、以下の関数が含まれています。
fakeauth
- アカウント リンク用の認証エンドポイントfaketoken
- アカウント リンク用のトークン エンドポイントsmarthome
- スマートホーム インテントのフルフィルメント エンドポイントreportstate
- デバイスの状態が変化したときに HomeGraph API を呼び出すupdateDevice
- 仮想デバイスが Report State のトリガーに使用するエンドポイント
Firebase に接続する
app-start
ディレクトリに移動し、Cloud 間インテグレーション プロジェクトで Firebase CLI を設定します。
cd app-start firebase use <project-id>
Firebase プロジェクトを設定する
Firebase プロジェクトを初期化します。
firebase init
CLI 機能の [Realtime Database]、[Functions]、Firebase Hosting を含む [Hosting] を選択します。
? Which Firebase CLI features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices. ❯◉ Realtime Database: Configure a security rules file for Realtime Database and (optionally) provision default instance ◯ Firestore: Configure security rules and indexes files for Firestore ◉ Functions: Configure a Cloud Functions directory and its files ◉ Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys ◯ Hosting: Set up GitHub Action deploys ◯ Storage: Configure a security rules file for Cloud Storage ◯ Emulators: Set up local emulators for Firebase products ◯ Remote Config: Configure a template file for Remote Config ◯ Extensions: Set up an empty Extensions manifest
これにより、プロジェクトに必要な API と機能が初期化されます。
プロンプトが表示されたら、Realtime Database を初期化します。データベース インスタンスのデフォルトの場所を使用できます。
? It seems like you haven't initialized Realtime Database in your project yet. Do you want to set it up? Yes ? Please choose the location for your default Realtime Database instance: us-central1
スターター プロジェクトのコードを使用しているため、セキュリティ ルールのデフォルト ファイルを選択します。既存のデータベース ルール ファイルを上書きしないよう注意してください。
? File database.rules.json already exists. Do you want to overwrite it with the Realtime Database Security Rules for <project-ID>-default-rtdb from the Firebase Console? No
プロジェクトを再初期化する場合は、コードベースを初期化するのか上書きするのかを尋ねられたら、[上書き] を選択します。
? Would you like to initialize a new codebase, or overwrite an existing one? Overwrite
関数を設定する際は、デフォルト ファイルを使用する必要があります。プロジェクト サンプル内の既存の index.js ファイルと package.json ファイルを上書きしないよう注意してください。
? What language would you like to use to write Cloud Functions? JavaScript ? Do you want to use ESLint to catch probable bugs and enforce style? No ? File functions/package.json already exists. Overwrite? No ? File functions/index.js already exists. Overwrite? No
プロジェクトを再初期化する場合は、functions/.gitignore を初期化するか上書きするかを尋ねられたら [No] を選択します。
? File functions/.gitignore already exists. Overwrite? No
? Do you want to install dependencies with npm now? Yes
最後に Hosting を設定し、プロジェクト コード内の public
ディレクトリと既存の index.html ファイルが使用されるようにします。ESLint の使用を求められたら、[いいえ] を選択します。
? What do you want to use as your public directory? public ? Configure as a single-page app (rewrite all urls to /index.html)? Yes ? Set up automatic builds and deploys with GitHub? No ? File public/index.html already exists. Overwrite? No
ESLint が誤って有効になっている場合は、次の 2 つの方法で無効にできます。
- GUI を使用して、プロジェクトの
../functions
フォルダに移動し、非表示ファイル.eslintrc.js
を選択して削除します。名前が似ている.eslintrc.json
と混同しないでください。 - コマンドラインを使用する:
cd functions rm .eslintrc.js
Firebase の構成が正しく完了していることを確認するには、firebase.json
ファイルを washer-done
ディレクトリから washer-start
ディレクトリにコピーし、washer-start
にあるファイルを上書きします。
washer-start
ディレクトリで:
cp -vp ../washer-done/firebase.json .
Firebase にデプロイする
これで依存関係のインストールとプロジェクトの設定が完了し、アプリを実行する準備が整いました。
firebase deploy
コンソールに次のような出力が表示されます。
... ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/<project-id>/overview Hosting URL: https://<project-id>.web.app
このコマンドによって、いくつかの Cloud Functions for Firebase とともにウェブアプリがデプロイされます。
ブラウザで Hosting URL(https://<project-id>.web.app
)を開き、ウェブアプリを表示します。次のようなインターフェースが表示されます。
このウェブ UI は、デバイスの状態を表示したり変更したりするためのサードパーティ プラットフォームを表したものです。データベースへのデバイス情報の入力を開始するには、[UPDATE](更新)をクリックします。ページの表示は変化しませんが、洗濯機の現在の状態がデータベースに保存されます。
次は、デベロッパー コンソールを使用して、デプロイしたクラウド サービスを Google アシスタントにリンクします。
Developer Console プロジェクトを構成する
[Develop] タブで、インタラクションの [Display Name](表示名)を追加します。この名前は Google Home アプリに表示されます。
[アプリのブランディング] で、アプリアイコンの png
ファイルをアップロードします。サイズは 144 x 144 ピクセルで、名前は
にします。
アカウント リンクを有効にするには、以下のアカウント リンク設定を使用します。
クライアント ID |
|
クライアント シークレット |
|
認証 URL |
|
トークンの URL |
|
[Cloud fulfillment URL](クラウド フルフィルメント URL)に、スマートホーム インテントのフルフィルメントを提供する Cloud Functions の関数の URL を入力します。
https://us-central1-
[Save] をクリックしてプロジェクトの設定を保存し、[Next: Test](次へ: テスト)をクリックしてプロジェクトでのテストを有効にします。
これで、デバイスの状態とアシスタントをリンクするために必要な Webhook の実装を開始できるようになりました。
Google アシスタントにリンクする
Cloud 間インテグレーションをテストするには、プロジェクトを Google アカウントにリンクする必要があります。これにより、同じアカウントにログインしている Google アシスタント画面と Google Home アプリでテストできるようになります。
- スマートフォンで Google アシスタントの設定を開きます。なお、コンソールと同じアカウントでログインする必要があります。
- [Google アシスタント] > [設定] > [スマートホーム]([アシスタント] の下)に移動します。
- 右上の検索アイコンをクリックします。
- [test] 接頭辞を使用してテストアプリを検索し、特定のテストアプリを見つけます。
- そのアイテムを選択します。Google アシスタントがサービスで認証を行い、
SYNC
リクエストを送信してデバイスのリストをユーザーに提供するようサービスに依頼します。
Google Home アプリを開いて、洗濯機デバイスが表示されることを確認します。
Google Home アプリで、音声コマンドを使用して洗濯機を操作できることを確認します。また、クラウド フルフィルメントのフロントエンド ウェブ UI で、デバイスの状態の変化を確認します。
これで、統合にローカル フルフィルメントを追加できるようになりました。
4. クラウド フルフィルメントを更新する
ローカル フルフィルメントをサポートするには、otherDeviceIds
というデバイスごとの新しいフィールドを、デバイス固有のローカル識別子を格納するクラウド SYNC
レスポンスに追加する必要があります。このフィールドは、デバイスをローカルに制御できるかどうかも示します。
次のコード スニペットに示すように、otherDeviceIds
フィールドを SYNC
レスポンスに追加します。
functions/index.js
app.onSync((body) => {
return {
requestId: body.requestId,
payload: {
agentUserId: '123',
devices: [{
id: 'washer',
type: 'action.devices.types.WASHER',
traits: [ ... ],
name: { ... },
deviceInfo: { ... },
willReportState: true,
attributes: {
pausable: true,
},
otherDeviceIds: [{
deviceId: 'deviceid123',
}],
}],
},
};
});
更新したプロジェクトを Firebase にデプロイします。
firebase deploy --only functions
デプロイが完了したら、ウェブ UI に移動してツールバーの [更新 ] ボタンをクリックします。これにより Request Sync 操作がトリガーされ、更新された SYNC
レスポンス データがアシスタントに送信されます。
5. ローカル フルフィルメントを設定する
このセクションでは、ローカル フルフィルメントに必要な設定オプションを Cloud 間インテグレーションに追加します。開発中は、ローカル フルフィルメント アプリを Firebase Hosting に公開し、Google Home デバイスからアクセスしてダウンロードできるようにします。
Google Home Developer Console で、画面左側の [Project] > [Cloud-to-cloud] に移動し、統合の [Edit] を選択します。[設定] ページで [ローカル フルフィルメント] までスクロールし、設定をオンにします。各テスト URL フィールドに次の URL を入力し、プロジェクト ID を挿入して [Save] をクリックします。
https://<project-id>.web.app/local-home/index.html
次に、Google Home デバイスがローカルのスマート デバイスを検出する方法を定義する必要があります。ローカルホーム プラットフォームは、mDNS、UPnP、UDP のブロードキャストなど、さまざまなデバイス検出プロトコルに対応しています。ここでは、UDP ブロードキャストを使用してスマート洗濯機を検出します。
[デバイス検出] で [+ スキャン構成を追加] をクリックして、新しいスキャン構成を追加します。プロトコルとして UDP を選択し、以下の属性を入力します。
項目 | 説明 | 推奨値 |
検出アドレス | UDP 検出アドレス |
|
ブロードキャスト ポート | Google Home が UDP ブロードキャストを送信するポート |
|
リスニング ポート | Google Home がレスポンスをリッスンするポート |
|
検出パケット | UDP ブロードキャスト データのペイロード |
|
最後に、ウィンドウの下部にある [保存] をクリックして変更内容を公開します。
6. ローカル フルフィルメントを実装する
Local Home SDK の型定義パッケージを使用して、TypeScript でローカル フルフィルメント アプリを開発します。スターター プロジェクトで提供されているスケルトンの中身を見てみましょう。
local/index.ts
/// <reference types="@google/local-home-sdk" />
import App = smarthome.App;
import Constants = smarthome.Constants;
import DataFlow = smarthome.DataFlow;
import Execute = smarthome.Execute;
import Intents = smarthome.Intents;
import IntentFlow = smarthome.IntentFlow;
...
class LocalExecutionApp {
constructor(private readonly app: App) { }
identifyHandler(request: IntentFlow.IdentifyRequest):
Promise<IntentFlow.IdentifyResponse> {
// TODO: Implement device identification
}
executeHandler(request: IntentFlow.ExecuteRequest):
Promise<IntentFlow.ExecuteResponse> {
// TODO: Implement local fulfillment
}
...
}
const localHomeSdk = new App('1.0.0');
const localApp = new LocalExecutionApp(localHomeSdk);
localHomeSdk
.onIdentify(localApp.identifyHandler.bind(localApp))
.onExecute(localApp.executeHandler.bind(localApp))
.listen()
.then(() => console.log('Ready'))
.catch((e: Error) => console.error(e));
ローカル フルフィルメントの中心となるコンポーネントは smarthome.App
クラスです。スターター プロジェクトでは、IDENTIFY
インテントと EXECUTE
インテントのハンドラをアタッチし、listen()
メソッドを呼び出してアプリの準備ができたことを Local Home SDK に通知します。
IDENTIFY ハンドラを追加する
Local Home SDK は、Developer Console でのスキャン設定に基づいて、ローカル ネットワーク上の未確認のデバイスを検出して IDENTIFY
ハンドラをトリガーします。
一致するデバイスが検出された場合は、スキャン結果のデータに基づいて identifyHandler
を呼び出します。アプリでは、UDP ブロードキャストを使用してスキャンを行い、ローカル デバイスによって送信されたレスポンス ペイロードを含むスキャンデータを IDENTIFY
ハンドラに提供します。
ハンドラは、ローカル デバイス固有の識別子を含む IdentifyResponse
インスタンスを返します。次のコードを identifyHandler
メソッドに追加することで、ローカル デバイスからの UDP レスポンスを処理し、適切なローカル デバイス ID を特定できます。
local/index .ts
identifyHandler(request: IntentFlow.IdentifyRequest):
Promise<IntentFlow.IdentifyResponse> {
console.log("IDENTIFY intent: " + JSON.stringify(request, null, 2));
const scanData = request.inputs[0].payload.device.udpScanData;
if (!scanData) {
const err = new IntentFlow.HandlerError(request.requestId,
'invalid_request', 'Invalid scan data');
return Promise.reject(err);
}
// In this codelab, the scan data contains only local device ID.
const localDeviceId = Buffer.from(scanData.data, 'hex');
const response: IntentFlow.IdentifyResponse = {
intent: Intents.IDENTIFY,
requestId: request.requestId,
payload: {
device: {
id: 'washer',
verificationId: localDeviceId.toString(),
}
}
};
console.log("IDENTIFY response: " + JSON.stringify(response, null, 2));
return Promise.resolve(response);
}
なお、デバイスをユーザーのホームグラフのローカル フルフィルメントで利用できることを示すためには、verificationId
フィールドが SYNC
レスポンスのいずれかの otherDeviceIds
値と一致している必要があります。一致しているデバイスは検証済みとなり、ローカル フルフィルメントで利用できるものと見なされます。
EXECUTE ハンドラを追加する
Local Home SDK は、ローカル フルフィルメントをサポートするデバイスがコマンドを受け取ると EXECUTE
ハンドラをトリガーします。ローカル インテントのコンテンツは、クラウド フルフィルメントに送信される EXECUTE
インテントと同等であるため、インテントをローカル処理するためのロジックはクラウドでの処理方法と似ています。アクション
アプリは、TCP/UDP ソケットまたは HTTP(S)リクエストを使用してローカル デバイスと通信できます。この Codelab では、プロトコルとして HTTP を使用して仮想デバイスを制御します。ポート番号は、index.ts
で SERVER_PORT
変数として定義されています。
次のコードを executeHandler
メソッドに追加することで、受信コマンドを処理して HTTP でローカル デバイスに送信できます。
local/index.ts
executeHandler(request: IntentFlow.ExecuteRequest):
Promise<IntentFlow.ExecuteResponse> {
console.log("EXECUTE intent: " + JSON.stringify(request, null, 2));
const command = request.inputs[0].payload.commands[0];
const execution = command.execution[0];
const response = new Execute.Response.Builder()
.setRequestId(request.requestId);
const promises: Array<Promise<void>> = command.devices.map((device) => {
console.log("Handling EXECUTE intent for device: " + JSON.stringify(device));
// Convert execution params to a string for the local device
const params = execution.params as IWasherParams;
const payload = this.getDataForCommand(execution.command, params);
// Create a command to send over the local network
const radioCommand = new DataFlow.HttpRequestData();
radioCommand.requestId = request.requestId;
radioCommand.deviceId = device.id;
radioCommand.data = JSON.stringify(payload);
radioCommand.dataType = 'application/json';
radioCommand.port = SERVER_PORT;
radioCommand.method = Constants.HttpOperation.POST;
radioCommand.isSecure = false;
console.log("Sending request to the smart home device:", payload);
return this.app.getDeviceManager()
.send(radioCommand)
.then(() => {
const state = {online: true};
response.setSuccessState(device.id, Object.assign(state, params));
console.log(`Command successfully sent to ${device.id}`);
})
.catch((e: IntentFlow.HandlerError) => {
e.errorCode = e.errorCode || 'invalid_request';
response.setErrorState(device.id, e.errorCode);
console.error('An error occurred sending the command', e.errorCode);
});
});
return Promise.all(promises)
.then(() => {
return response.build();
})
.catch((e) => {
const err = new IntentFlow.HandlerError(request.requestId,
'invalid_request', e.message);
return Promise.reject(err);
});
}
TypeScript アプリをコンパイルする
TypeScript コンパイラをダウンロードしてアプリをコンパイルするため、local/
ディレクトリに移動して次のコマンドを実行します。
cd local npm install npm run build
これにより、index.ts
(TypeScript)のソースがコンパイルされ、以下のファイルが public/local-home/
ディレクトリに格納されます。
bundle.js
- ローカルアプリと依存関係を含むコンパイル済み JavaScript の出力。index.html
- デバイスでのテストでアプリの配信に使用するローカル ホスティング ページ。
テスト プロジェクトをデプロイする
更新したプロジェクト ファイルを Firebase Hosting にデプロイします。これにより、Google Home デバイスからアクセスできるようになります。
firebase deploy --only hosting
7. スマート洗濯機を起動する
次に、ローカル フルフィルメント アプリとスマート洗濯機の間の通信をテストします。この Codelab スターター プロジェクトでは、Node.js で記述した仮想のスマート洗濯機を使って、スマート洗濯機のローカルでの操作をシミュレーションします。
デバイスを設定する
デベロッパー コンソールでデバイス検出のスキャン設定に適用したのと同じ UDP パラメータを使用するため、仮想デバイスを設定する必要があります。また、報告するローカル デバイス ID と、デバイスの状態が変更されたときに Report State イベントに使用する Cloud-to-Cloud 統合のプロジェクト ID を、仮想デバイスに指定する必要があります。
パラメータ | 推奨値 |
deviceId |
|
discoveryPortOut |
|
discoveryPacket |
|
projectId | Cloud 間インテグレーションのプロジェクト ID |
デバイスを起動する
virtual-device/
ディレクトリに移動し、引数として設定パラメータを渡してデバイス スクリプトを実行します。
cd virtual-device npm install npm start -- \ --deviceId=deviceid123 --projectId=<project-id> \ --discoveryPortOut=3311 --discoveryPacket=HelloLocalHomeSDK
デバイス スクリプトが、想定どおりのパラメータで実行されたことを確認します。
(...): UDP Server listening on 3311 (...): Device listening on port 3388 (...): Report State successful
8. TypeScript アプリをデバッグする
次のセクションでは、Google Home デバイスがローカル ネットワーク経由で仮想スマート洗濯機をスキャンして適切に識別し、コマンドを送信できることを確認します。Google Chrome デベロッパー ツールを使用すると、Google Home デバイスへの接続、コンソール ログの確認、TypeScript アプリのデバッグを行うことができます。
Chrome デベロッパー ツールを接続する
次の手順に沿って、デバッガをローカル フルフィルメント アプリに接続します。
- Google Home デバイスが、Developer Console プロジェクトにアクセスできるユーザーにリンクしていることを確認します。
- Google Home デバイスを再起動します。これにより、デベロッパー コンソールで設定した HTML の URL とスキャン設定が取得されます。
- 開発マシンで Chrome を起動します。
- 新しい Chrome タブを開き、アドレス フィールドに「
chrome://inspect
」と入力して、インスペクタを起動します。
ページ上にデバイスのリストが表示され、Google Home デバイスの名前の下にアプリの URL が表示されます。
インスペクタを起動する
アプリの URL の下にある [Inspect](検査)をクリックして Chrome デベロッパー ツールを起動します。[Console](コンソール)タブを選択し、TypeScript アプリによって出力された IDENTIFY
インテントの内容が表示されることを確認します。
この出力を見ると、ローカル フルフィルメント アプリが仮想デバイスを正常に検出して識別できたことがわかります。
ローカル フルフィルメントをテストする
Google Home アプリのタップ コントロールまたは音声コマンドを使用して、Google Home デバイスにコマンドを送信します。次に例を示します。
「OK Google, 洗濯機をオンにして。」
「OK Google, 洗濯を開始して。」
「OK Google, 洗濯機を止めて。」
これにより、プラットフォームから TypeScript アプリに EXECUTE
インテントが送信されます。
それぞれのコマンドによって、ローカル スマート洗濯機の状態が変化することを確認します。
... ***** The washer is RUNNING ***** ... ***** The washer is STOPPED *****
9. 完了
これで、Local Home SDK を使用して、ローカル フルフィルメントをクラウド間統合に統合しました。
その他の情報
他にも以下のことを試してみてください。
- スキャン設定を変更しても機能するかどうか。たとえば、別の UDP ポートや検出パケットを使用してみます。
- 仮想スマート デバイスのコードベースを修正する。たとえば Raspberry Pi のような組み込みデバイス上で実行できるようにしたり、LED やディスプレイを使って現在の状態を可視化したりしてみます。