RaspberryPiとFirebaseでリモコンをWebアプリに複製する

## 完成品
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">赤外線の登録も楽チン! <a href="https://t.co/cUIZGx2qUm">pic.twitter.com/cUIZGx2qUm</a></p>&mdash; なべこう (@nabekou29) <a href="https://twitter.com/nabekou29/status/1185092479013376000?ref_src=twsrc%5Etfw">October 18, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## やりたいこと

タイトル通り、**「リモコンの web アプリへの複製(移植)」**です。

ラズパイを使ったスマホからの赤外線の送信は多くの人が記事をあげてくださっています。
ラズパイで信号を登録 → Slack, GoogleAssistant を通して送信する。という手法を取っている記事が多く見られます。

この方法の欠点として、
- **多くの信号の登録するのが面倒**
- **家で操作するのに文字を入力したりするのは億劫**

という点があります。そこで今回は、

- **信号の受信・登録をスマホからできるようにする**
- **なるべく実際のリモコンに近い UI にして操作性をあげる**

この 2 点を意識して作っていこうを思います。

## 使うもの

- RaspberryPi zero w (Wi-Fi に繋げられるモデルなら大丈夫)
- 赤外線の送信/受信モジュール
- その他細かい電子部品(後述)

## 方針

### どうやって赤外線を送信するか？

IR Record and Playback(IRRP)を使います。

同じく赤外線の送信に使える「LIRC」とういうものがありますが、扱いづらかったため IRRP を使います。
IRRP はラズパイ zero だと少し処理が重いですが、許容範囲なので良しとしましょう。

### どうやってスマホと連携するか？

[Firebase Realtime Database](https://firebase.google.com/docs/database/?hl=ja)を使います。

通信するのになんで DB なんだよっ!て感じですが、Realtime Database はリアルタイムなデータの更新されたタイミングで任意の処理を実行できます。つまり、ソケット通信的なことが実現できます。

### 自分以外が使えないようにするには？

上で Firebase を使ったので答えは出ていますが...、[Firebase Authentication](https://firebase.google.com/docs/auth/?hl=ja)を使って認証をします。

特定の Google アカウントでのみ使えるようにします。Database の更新のルールを変更するだけで済みますね。

# 実装

## コマンドで操作する

### 準備

IR Record and Playback の準備をする
参考: http://kobore.net/remote_control.html

```pigpiodをインストールする.sh
$ sudo apt update
$ sudo apt upgrade
$ sudo apt install pigpio python3-pigpio
$ sudo systemctl enable pigpiod.service    # ラズパイ再起動時に pigpiod を自動的に起動させます
$ sudo systemctl start pigpiod             # いますぐ pigpiod を起動
```

```GPIOの設定.sh
$ echo 'm 17 w   w 17 0   m 26 r   pud 26 u' > /dev/pigpio
$ crontab -e
# 行末に以下を追記
# @reboot until echo 'm 17 w   w 17 0   m 26 r   pud 26 u' > /dev/pigpio; do sleep 1s; done
```

GPIO17 を送信, 26 を受信に使う場合の設定。

```irrpのダウンロード.sh
$ curl http://abyz.me.uk/rpi/pigpio/code/irrp_py.zip | zcat > irrp.py
```

### 回路

[格安スマートリモコンの作り方](https://qiita.com/takjg/items/e6b8af53421be54b62c9)
**回路に必要な部品はこちらが参考になります**
こちらの記事を参考に組み上げる。回路は詳しくないので、MOSFET だけ同じものを購入してあとはその辺にあったもので作りました。色々調べていると多くの方がこの記事を参考にしていた。とてもいい記事。

回路が組めない場合や面倒な場合は以下のようなモジュールを買えば大丈夫です。
[GROVE - 赤外線発信器](https://www.switch-science.com/catalog/885/), [GROVE - 赤外線受信器](https://www.switch-science.com/catalog/910/)

### 動かしてみる

```受信.sh
$ python3 irrp.py -r -g26 -f code.json aircon:on --no-confirm --post 130
# 受信モジュールに向かって信号を送る
Recording
Press key for 'aircon:on'
Okay
```

```送信.sh
$ python3 irrp.py -p -g17 -f code.json aircon:on
```

エアコンの運転が開始していれば成功です。

ちなみに code.json の中身はこんな感じ

```code.json
{"aircon:on": [3492, 1738, 長いので省略..., 441, 1302, 441]}
```

あとは外部からの通信でこのコマンドを実行できるようにすれば良さそうですね。

## スマホから操作する

**Firestore には以下のようなデータを登録するようにします**

- remocon ... リモコン
  - 名前
- signal ... 信号 - 名前 - コード（IRRP で json に保存される数字の配列） - リモコン ID
  ![スクリーンショット 2019-10-18 10.00.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258777/45d6bfcb-612e-99f8-076d-bd339fe0c743.png)
  ![スクリーンショット 2019-10-18 10.01.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258777/fcfa7547-04c1-b984-65bd-caefb38882ec.png)

**Database は以下の 3 つでデータのやり取りをします**

- スマホ → ラズパイ
  - send_signal ... 信号の送信
  - receive_new_signal ... 信号の受信(開始)
- ラズパイ → スマホ 
  - received_signal ... 信号の受信(完了)
  ![スクリーンショット 2019-10-18 9.59.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258777/f2e3c86a-447e-b57c-6ec8-836e01bf4396.png)

以下に送信と受信のメイン部分のコードを載せます。

### 送受信の流れ
だいたいこんな感じ
<img width="531" alt="スクリーンショット 2019-10-18 16.39.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258777/ea9ddc22-b914-a132-6b0f-aba09dbe2d32.png">

### 送信

```client.ts
/**
 * 指定分後に信号を送信します
 * @param signalId 信号ID
 * @param minutes 何分後か
 */
export const sendSignal = async (
  signalId: string,
  minutes: number = 0
): Promise<any> => {
  const data = {
    signal_id: signalId,
    minutes,
    timestamp: firebase.database.ServerValue.TIMESTAMP,
  };
  return await firebase
    .database()
    .ref('send_signal')
    .set({ data });
};
```

DB の`send_signal`の値を書き換えているだけですね。
`set(data)`ではなく`set({data})`にしてください。（理由は後述）

```server.ts
// 送信
admin
  .database()
  .ref('/send_signal')
  .on('child_changed', (snapshot: admin.database.DataSnapshot | null) => {
    if (!snapshot) {
      return;
    }
    const data = snapshot.val();
    if (data.minutes === 0) {
      send(data.signal_id);
    } else {
      const date = dayjs().add(data.minutes, 'minute');
      schedule.scheduleJob(date.toDate(), () => {
        send(data.signal_id);
      });
    }
  });
};

/** 信号の送信 */
async function send(id: string) {
  const command = `python3 ${CWD}/irrp.py -p -g${SEND_GPIO_NO} -f ${CWD}/${CODE_FILE} ${id}`;
  const text = fs.readFileSync(`${CWD}/${CODE_FILE}`, 'utf8');
  const codes = text ? JSON.parse(text) : {};

  // jsonファイルに指定されたIDの信号が登録されていなければ、新たに登録する
  if (!codes.hasOwnProperty || !codes.hasOwnProperty(id)) {
    const remoconDoc = await admin
      .firestore()
      .collection('signal')
      .doc(id)
      .get();
    if (remoconDoc.exists) {
      const data = remoconDoc.data();
      if (!data) {
        throw new Error();
      }
      codes[id] = data.code;
      fs.writeFileSync(`${CWD}/${CODE_FILE}`, JSON.stringify(codes));
    }
  }
  console.log(`send: ${id}`);
  execSync(command);
}
```

`child_changed`で client 側で`set({data})`したタイミングで処理を実行します。`child_changed`は差分を拾って処理するため、client 側の set 時に`{data}`としすることで階層を 1 つ下げて、セットした値の全てを差分として認識させています。

### 受信

```client.ts
/**
 * 指定分後に信号を送信します
 * @param signalId 信号ID
 * @param minutes 何分後か
 */
export const sendSignal = async (
  signalId: string,
  minutes: number = 0
): Promise<any> => {
  const data = {
    signal_id: signalId,
    minutes,
    timestamp: firebase.database.ServerValue.TIMESTAMP,
  };
  return await firebase
    .database()
    .ref('send_signal')
    .set({ data });
};
```

```server.ts
// 受信・登録
admin
  .database()
  .ref('/receive_new_signal')
  .on('child_changed', (snapshot: admin.database.DataSnapshot | null) => {
    if (!snapshot) {
      return;
    }
    const data = snapshot.val();
    receive(data.timeout).then((signal: number[]) => registerSignal(signal));
  });

/** 信号の受信 */
async function receive(timeout: number): Promise<number[]> {
  const command = `python3 ${CWD}/irrp.py -r -g${RECEIVE_GPIO_NO} -f ${CWD}/${TMP_CODE_FILE} ${TMP_CODE_NAME} --no-confirm --post 130`;
  console.log('receiving...');
  execSync(command, { timeout });
  console.log('received!');

  const text = fs.readFileSync(`${CWD}/${TMP_CODE_FILE}`, 'utf8');
  const codes = text ? JSON.parse(text) : {};
  return codes[TMP_CODE_NAME];
}

/** 受信した信号の登録 */
export const registerSignal = async (code: number[]): Promise<any> => {
  const data = {
    code,
    timestamp: admin.database.ServerValue.TIMESTAMP,
  };
  return await admin
    .database()
    .ref('received_signal')
    .set({ data });
};
```

基本的なことは送信と同じです。
`receive_new_signal`をトリガーに受信を開始して、受信したタイミングで`received_signal`に値をセットします。
受信に失敗する可能性があるのでタイムアウト時間を指定できるようにしています。

### ルールの修正

最後に自分以外の人が操作できないようにFirebaseのルールを修正しましょう。

```firestore.rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if isValidUser();
    }
  }

  function isValidUser() {
    return request.auth.uid != null && request.auth.token.email == '自身のGmail';
  }
}
```

```database.rules.json
{
  "rules": {
    ".read": true,
    ".write": "auth.provider == 'google' && auth.token.email == '自身のGmail'"
  }
}
```

### Webアプリの作成
[Next.js](https://nextjs.org/)を勉強したかったため、Next.jsで作成してFirebaseにデプロイしました。
実際のコードはこちら → https://github.com/nabekou29/smart_remocon

機能は以下の通り

- リモコンとそのリモコンの信号の一覧の表示
- 信号の送信・タイマーを設定しての送信
- リモコン・信号の追加 

## 作ってみて

外からの操作に関しては、これから寒くなってきたのでこれからに期待です☺️
きっとこれからの冬に活躍してくれることを祈ってます。
家からの操作は少しのラグが気になるものの、ベッドにリモコンを持っていく必要が無くなったり、玄関から明かりを消せるのが便利だったりします。

今回 Firebase の DB をデータの保存ではなく通信に使ったのがかなり便利でした。IFTTT に出会った時と同程度の感動を覚えました。
今後もラズパイを使う時にはお世話になりそうです。
時間指定の実行であったり、タイマーの履歴が見れたりするともっと良さそうですね。

## 参考サイト

[Raspbian stretch Lite で初期設定](https://qiita.com/syasuda/items/28d4451fcea598949aac)
[Raspberry Pi と Firebase でブラウザからエアコン操作](https://qiita.com/horo/items/1b1549ff8354e9788c9e)
[格安スマートリモコンの作り方](https://qiita.com/takjg/items/e6b8af53421be54b62c9)
[ラズパイ 2 で学習リモコンを作る.](http://kobore.net/remote_control.html)
[TypeScript + Node.js プロジェクトのはじめかた2019](https://qiita.com/notakaos/items/3bbd2293e2ff286d9f49)