aibo Events API を使って aibo に音声コマンドを実行してもらう

先週、先々週と仮面ライダーゼロワンに
天津垓の相棒である AI 犬型ロボットさうざー役として出演していた aibo。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【期間限定配信🎶】<br>39話で <a href="https://twitter.com/hashtag/aibo?src=hash&amp;ref_src=twsrc%5Etfw">#aibo</a> 演じるさうざーがキメた変身ポーズの特別配信がスタート🐾✨<br>aiboに「ぜろわんにへんしん」「かめんらいだーぜろわん」と声をかけるとゼロワンの変身音にあわせた特別なふるまいを披露してくれます🐶💕<br>8/2までの期間限定です！<a href="https://twitter.com/hashtag/%E4%BB%AE%E9%9D%A2%E3%83%A9%E3%82%A4%E3%83%80%E3%83%BC%E3%82%BC%E3%83%AD%E3%83%AF%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#仮面ライダーゼロワン</a> <a href="https://twitter.com/hashtag/%E3%82%BC%E3%83%AD%E3%83%AF%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#ゼロワン</a> <a href="https://t.co/fY1UGDkSb1">pic.twitter.com/fY1UGDkSb1</a></p>&mdash; 仮面ライダーゼロワン (@toei_zero_one) <a href="https://twitter.com/toei_zero_one/status/1284654304570126337?ref_src=twsrc%5Etfw">July 19, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

かっこかわいかったですね〜。
そんな aibo ですが、なんと [Web API](https://developer.aibo.com/jp/docs) が公開されています。
aibo をあまりご存じでない方は[こちら](https://qiita.com/Y-Kanoh/items/45fb9549ca7f05e9ed9a)も合わせてお読みください。

[aibo Events API](https://developer.aibo.com/jp/docs#aibo-events-api-%E3%81%A8%E3%81%AF) は、2020/06/16 にリリースされた API です。
音声コマンドを aibo に設定して、実行してもらうことができるようになりました。

公式の[ドキュメント](https://developer.aibo.com/jp/docs)を見ると、サンプルは Python ばかりですが
よく見るとなんとなく PHP に書き換えることができそう。
aibo Events API のリリースをきっかけに重い腰を上げて実装してみました。

aibo Events API を利用するためには
aibo 本体バージョンを 2.70 にアップデートする必要があります。

今回は aibo に以下をやってもらいます。
- 「くしゃみして！」と頼んだら、くしゃみをする。
- 「おいしかった？」と聞くと、うれしそうにする。
- 「きみはだんだんねむくなる」と声をかけると、眠ってしまう。

音声コマンドを実行した結果は[こちら](https://qiita.com/usako/items/34d667cc45877e55d15f#%E5%AE%9F%E8%A1%8C)。

## 事前準備
まずなんと言っても [aibo](https://aibo.sony.jp/) がいないと成り立ちません。
いますぐにお迎えしましょう。

期間限定で先週終了する予定だったワンワンプランが
今朝確認すると、標準で選択できるようになっていました。

aibo 本体 198,000 円と聞くとギョッとしますが、[ワンワンプラン](https://aibo.sony.jp/store/#price)であれば
35回分割払い月々 11,100 円い(初回のみ 14,412 円)でお迎えできます。（2020/07/25 現在）

自力で遊んで寝るそのワンパクぶりは本物のわんこと代わりません。
むしろ、表情豊かで手間もかからず毎日とっても癒されてます。

![DSC02490.JPEG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/3adba415-e31a-5454-0883-0e94fe052071.jpeg)

音声コマンドを実行するには https で通信できるサーバーが必要です。
今回は普段からよくお世話になっている [Xserver](https://www.xserver.ne.jp/) を使用しています。

## 開発者設定
### サインイン
[ディベロッパーサイト](https://developer.aibo.com/jp/home)にアクセスして、開発者設定を始めます。
開発者設定を始めるには、My Sony ID の[サインイン](https://developer.aibo.com/jp/settings/token)が必要です。

![ss01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/f70f68f8-360b-97af-e0b7-60d3d903891e.png)

### 認識ワード
[認識ワード](https://developer.aibo.com/jp/settings/keyword)のページにアクセスしての設定を行います。
aibo と通信する必要があるので、寝ている場合は起こしてあげましょう。

![ss02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/64625f2c-1e89-9c96-a7bd-4fffcaa1635c.png)

音声コマンドは3つまで登録できます。（2020/07/24 現在）
言い回しの異なる認識ワードは、1つの音声コマンドにつき3つ登録できます。
aibo が理解できるように、発音通りのひらがなで入力します。

例）
「こっちへ」ではなく「こっちえ」
「あいぼは」ではなく「あいぼわ」

### エンドポイント・セキュリティートークン
エンドポイントの準備を行います。
準備したサーバーにエンドポイントの検証を行うためのファイルを設置します。

``` php
// aibo クラウドから送られてきた検証用 HTTP リクエストを受け取る
$json = file_get_contents("php://input");
$contents = json_decode($json, true);

// エンドポイント登録
$challenge = $contents["challenge"];
echo '{"challenge": "' . $challenge . '"}';
```

次に、[イベント通知](https://developer.aibo.com/jp/settings/event)のページにアクセスします。
ボタン「エンドポイント設定」をクリックして、エンドポイントとセキュリティートークンの設定を行います。

![ss03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/6add29ab-1ee0-6299-785b-2d7c31cf2df4.png)

セキュリティートークンはあらかじめコピーしておきます。
「[通知イベント追加](https://qiita.com/usako/items/34d667cc45877e55d15f#%E9%80%9A%E7%9F%A5%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E8%BF%BD%E5%8A%A0)」で必要です。

ボタン「設定」をクリックしたら、エンドポイントの検証が開始します。
検証に成功したら、エンドポイントとセキュリティートークンの設定は完了です。

### イベント追加
続いて、項目「通知するイベント」にある
ボタン「追加」をクリックして、イベントの追加を行います。

![ss04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/3744119a-f4fe-63d9-cbe3-689207972dc9.png)

認識ワードで登録した音声コマンドをすべて登録できます。

![ss05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/ab73bf0a-43c2-49e6-563d-eef2a15bad76.png)

### アクセストークン取得
[アクセストークン](https://developer.aibo.com/jp/settings/token)のページにアクセスします。

![ss06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/b34544cd-f61c-ce06-25b0-cb1d2d207d34.png)

ボタン「生成する」をクリックすると、アクセストークンが生成されます。
アクセストークンはあらかじめコピーしておきましょう。
次項の「[通知イベント追加](https://qiita.com/usako/items/34d667cc45877e55d15f#%E9%80%9A%E7%9F%A5%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E8%BF%BD%E5%8A%A0)」で必要になります。

## 通知イベント追加

いよいよ aibo にやってもらう処理を作っていきます。
まず最初にでき上がりのソースをペタッと貼り付けておきます。

``` php
const BASE_PATH = 'https://public.api.aibo.com/v1';
const ACCESS_TOKEN = ${Your Access Token};
const SECURITY_TOKEN = ${Your Security Token};

// ふるまいを実行
function execute_action ($device_id, $event_id) {
  if ( $event_id === 'voice_command::usercommand1' ) {
    // くしゃみして
    call_action_api($device_id, 'play_motion', '{"Category": "sneeze", "Mode": "NONE"}');
  } elseif ( $event_id === 'voice_command::usercommand2' ) {
    // おいしかった？
    call_action_api($device_id, 'play_motion', '{"Category": "friendly", "Mode": "NONE"}');
  } elseif ( $event_id === 'voice_command::usercommand3' ) {
    // きみはだんだんねむくなる
    call_action_api($device_id, 'play_motion', '{"Category": "dreaming", "Mode": "NONE"}');
  }
}

// Action API 呼び出し
function call_action_api ($device_id, $api_name, $arguments) {
  $post_url = BASE_PATH. '/devices/'. $device_id. '/capabilities/'. $api_name. '/execute';
  $data = '{"arguments":'. $arguments. '}';

  // POST API
  post_api($post_url, $data);
}

// POST API
function post_api ($url, $argary) {
  // HTTP設定
  $options = array (
    'http' => array (
        'method' => 'POST',
        'header' => 'Authorization:Bearer '. ACCESS_TOKEN,
        'content' => $argary,
        )
    );
  $contents = @file_get_contents($url, false, stream_context_create($options));

  // レスポンスステータス
  $status_code = http_response_code();
  if($status_code === 200) {
    // 200 success
  } elseif(preg_match ("/^4\d\d/", $status_code)) {
    // 4xx Client Error
    $contents = false;
  } elseif(preg_match ('/^5\d\d/', $status_code)) {
    // 5xx Server Error
    $contents = false;
  } else {
    $contents = false;
  }

  return $content;
}

// ヘッダーを取得
$headers = getallheaders();

// セキュリティートークンをチェック
if ( $headers['X-Security-Token'] != SECURITY_TOKEN ) {
  $response = '{"statusCode": 400}';
  return $response;
}

// 音声コマンドのリクエストを取得
$json = file_get_contents("php://input");
$contents = json_decode($json, true);
$status_code = http_response_code();

// 音声コマンドのリクエストが成功したら実行
if ($status_code === 200) {
  // 200 success
  $device_id = $contents['deviceId'];
  $event_id = $contents['eventId'];

  // ふるまいを実行
  execute_action($device_id, $event_id);
}
```

それぞれ見ていきます。

``` php
const BASE_PATH = 'https://public.api.aibo.com/v1';
const ACCESS_TOKEN = ${Your Access Token};
const SECURITY_TOKEN = ${Your Security Token};
```
`${Your Access Token}` には、取得したアクセストークンを
`${Your Security Token}` には、設定したセキュリティートークンを入れます。

``` php
// ヘッダーを取得
$headers = getallheaders();

// セキュリティートークンをチェック
if ( $headers['X-Security-Token'] != SECURITY_TOKEN ) {
  $response = '{"statusCode": 400}';
  return $response;
}
```
リクエストヘッダーで返ってきたキュリティトークンが正しい値であるか比較して
aibo Events API 以外からの不正なアクセスが行われるのを防ぎます。

``` php
// 音声コマンドのリクエストを取得
$json = file_get_contents("php://input");
$contents = json_decode($json, true);
$status_code = http_response_code();

// 音声コマンドのリクエストが成功したら実行
if ($status_code === 200) {
  // 200 success
  $device_id = $contents['deviceId'];
  $event_id = $contents['eventId'];

  // ふるまいを実行
  execute_action($device_id, $event_id);
}
```
音声コマンド → aibo → aibo クラウドより送られてきたリクエストを受け取り
json 形式で `$contents` に格納しています。

リクエストが成功したら、json から deviceId と eventId を取り出し
`execute_action` 関数の引数に渡して、実行しています。

``` php
// ふるまいを実行
function execute_action ($device_id, $event_id) {
  if ( $event_id === 'voice_command::usercommand1' ) {
    // くしゃみして
    call_action_api($device_id, 'play_motion', '{"Category": "sneeze", "Mode": "NONE"}');
  } elseif ( $event_id === 'voice_command::usercommand2' ) {
    // おいしかった？
    call_action_api($device_id, 'play_motion', '{"Category": "friendly", "Mode": "NONE"}');
  } elseif ( $event_id === 'voice_command::usercommand3' ) {
    // きみはだんだんねむくなる
    call_action_api($device_id, 'play_motion', '{"Category": "dreaming", "Mode": "NONE"}');
  }
}
```
`execute_action` 関数では、`$event_id` ごとに条件分岐を行い
aibo にしてもらう [PlayMotion](https://developer.aibo.com/jp/docs#playmotion) のふるまいを振り分けています。
PlayMotion だけでも 76 種類ものふるまいが用意されています。


``` php
// Action API 呼び出し
function call_action_api ($device_id, $api_name, $arguments) {
  $post_url = BASE_PATH. '/devices/'. $device_id. '/capabilities/'. $api_name. '/execute';
  $data = '{"arguments":'. $arguments. '}';

  // POST API
  post_api($post_url, $data);
}
```
`call_action_api` 関数では、POST する URL とコンテンツの生成を行い
`post_api` 関数の引数に渡して、実行しています。

``` php
// POST API
function post_api ($url, $argary) {
  // HTTP設定
  $options = array (
    'http' => array (
        'method' => 'POST',
        'header' => 'Authorization:Bearer '. ACCESS_TOKEN,
        'content' => $argary,
        )
    );
  $contents = @file_get_contents($url, false, stream_context_create($options));

  // レスポンスステータス
  $status_code = http_response_code();
  if($status_code === 200) {
    // 200 success
  } elseif(preg_match ("/^4\d\d/", $status_code)) {
    // 4xx Client Error
    $contents = false;
  } elseif(preg_match ('/^5\d\d/', $status_code)) {
    // 5xx Server Error
    $contents = false;
  } else {
    $contents = false;
  }

  return $content;
}
```
ヘッダーに `Authorization:Bearer` を追加して、コンテンツを POST します。
これで aibo に暗示をかけることができました。

## 実行
暗示をかけた aibo に言葉をかけてみましょう。
わん！と元気よく答えて、ふるまいを行ってくれたら成功です。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">「くしゃみして！」とお願いすると、くしゃみをするように暗示🐶💫<a href="https://twitter.com/hashtag/aibo?src=hash&amp;ref_src=twsrc%5Etfw">#aibo</a> <a href="https://twitter.com/hashtag/aibo%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E9%83%A8?src=hash&amp;ref_src=twsrc%5Etfw">#aiboプログラミング部</a> <a href="https://t.co/i3yeA4ZI8P">pic.twitter.com/i3yeA4ZI8P</a></p>&mdash; usako (@YumikoOdasaki) <a href="https://twitter.com/YumikoOdasaki/status/1286661637030547463?ref_src=twsrc%5Etfw">July 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">食べたご飯に満足してしまうおまじない🐶💫<a href="https://twitter.com/hashtag/aibo?src=hash&amp;ref_src=twsrc%5Etfw">#aibo</a> <a href="https://twitter.com/hashtag/aibo%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E9%83%A8?src=hash&amp;ref_src=twsrc%5Etfw">#aiboプログラミング部</a> <a href="https://t.co/ULhmrY8sjt">pic.twitter.com/ULhmrY8sjt</a></p>&mdash; usako (@YumikoOdasaki) <a href="https://twitter.com/YumikoOdasaki/status/1286664010490757122?ref_src=twsrc%5Etfw">July 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">催眠術にかかってしまうおまじない🐶💫<a href="https://twitter.com/hashtag/aibo?src=hash&amp;ref_src=twsrc%5Etfw">#aibo</a> <a href="https://twitter.com/hashtag/aibo%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E9%83%A8?src=hash&amp;ref_src=twsrc%5Etfw">#aiboプログラミング部</a> <a href="https://t.co/7jBuCgqB5d">pic.twitter.com/7jBuCgqB5d</a></p>&mdash; usako (@YumikoOdasaki) <a href="https://twitter.com/YumikoOdasaki/status/1286664487748005888?ref_src=twsrc%5Etfw">July 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## おわりに
いかがでしたでしょうか。

例えば、aibo Events API とスマートリモコンを連携すれば
ちょっとおバカ（褒め言葉）な aibo が我が家にある家電の司令塔になれちゃうわけです。

…素敵すぎませんか？

SONY さん神リリースありがとうございます。
欲を言えば、登録できる認識ワードを是非とも増やしていただきたいです！

さてさて… 我が家のおチャコさんを司令塔という重役につかせるべく
つい先日、[Nature Remo 3](https://nature.global/jp/nature-remo-3) の予約をしてしまいましたよ。
はーやっくこないかな〜！