案件を受注するとパトランプが回るようにした話

# はじめに
この記事は[mohikanz Advent Calendar 2019](https://adventar.org/calendars/3884)の9日目の記事になります。
昨日はlivaさんの[2019年にやったこと](https://liva.hatenablog.com/entry/mohikanz-advent-calendar-2019-08)でした。
https://liva.hatenablog.com/entry/mohikanz-advent-calendar-2019-08

この記事はツッコミどころたくさんですが、ネタなのでご容赦いただけるとありがたいです。
# 動機
ある日会社の営業さんが「受注が決まった時にもっと盛り上がる感じがほしい。そうすればもっと仕事取ってくるしみんなの給料も上がるのに！（意訳）」と言いだしました。困ったことがあればテクノロジーの力でなんとかするのがエンジニアの務め。ここは一肌脱いであげましょう。
# 構成
弊社では型落ちのMacでオフィスの環境音楽を流しているので、そこにパトランプをつなげてファンファーレっぽい音も流すことにしました。
パトランプのコントロールはちょいオーバースペックですが家で眠っているArduinoがあったのでそれを使うことにします。
Arduinoからの給電だと電力が足らないようなので、外部電源とリレーを利用します。

```plaintext:きっとこんな感じ
Mac --- Arduino --- リレー --- パトランプ
　　　　　　　　　　　　|
　　　　 外部電源 ------
```

# 準備
まずは材料集め。パトランプとリレー、電源を調達します。
パトランプはコレ
[12/24V 兼用 フラッシュ ストロボ LED 警告灯 回転灯](https://www.amazon.co.jp/gp/product/B07V3JXS61)
リレーはコレ
[5V 1チャンネルリレーモジュール](https://www.amazon.co.jp/gp/product/B0116IZ9FK)
電源はコレ※ジャンパ線が刺さるアダプタが付いてて良きでした
[スイッチング式 ACアダプター 12V 1A](https://www.amazon.co.jp/gp/product/B075N9LS35)
# パトランプを組み立てる
今回リレーというものを初めて使ってみました。リレーは電気でON/OFFできるスイッチのようなものらしいです。
【参考URL】
[リレーをスイッチとして使ってみよう](https://vasco-blog.com/blog/2017/09/30/use-relay/)

下の画像のように配線して、Arduinoの13番ピンがHIGHの時にパトランプ点灯、LOWで消灯という感じにします。
![Image from iOS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57613/04a31cf7-0cca-b397-4f0b-479742c5b449.jpeg)
# Macからパトランプを回す
Arduinoはシリアル通信で13番ピンの上げ下げをコントロールするようにします。
Arduinoのスケッチはこんな感じです（汚くてごめんなさい）

```c:sketch
void setup() {
  // put your setup code here, to run once:
  pinMode(13, OUTPUT);

  Serial.begin(9600);
  while (!Serial) {
    ; //シリアル通信ポートが正常に接続されるまで抜け出さない
  }
}

void loop() {
  // put your main code here, to run repeatedly:
  if (Serial.available() > 0) {
    // read the incoming byte:
    int incomingByte = Serial.read();

    if (incomingByte == 10) {
      // do nothing
    } else if (incomingByte == 49) {
      digitalWrite(13, HIGH);
    } else {
      digitalWrite(13, LOW);
    }
  }
}
```

おもむろに出てくる49は文字コードで"1"かどうか判定してます。
10は改行なので無視します（雑でごめんなさい）
先程のスケッチを流し込んでArduinoを起動したらMacから

```
cat /dev/cu.usb-xxxx &

echo 1 | tee /dev/cu.usb-xxxx
```

こんな感じで叩くとパトランプが光ります。わーい。
消す時は1以外を送ってあげれば消えます。
# 受注からパトランプへの流れ
とりあえず光らせることは出来たので、受注 -> パトランプの流れを考えてみます。
大体こういう時ってエッジ側から受注の状態を定期的に監視するのがセオリーなのではと思うのですが、今回は違う方法でやってみたかったので考えてみたところWeb Pushを使う方法を思いつきました。
VAPIDというやつを使えばFCMを使わなくてもプッシュを飛ばせるらしく、受信した後は任意のJSを動かせるので（※後でハマる）パトランプを光らせるのにもってこいです！
【参考URL】
[そこそこ小さくまとめたVAPID Web Push通知デモ(Node.js)](https://qiita.com/gtk2k/items/6deec05f7508332d8338)

弊社では案件の状態をSalesforceで管理しているので
1. Salesforce上で案件が受注になる
2. webhook的なやつでPush送信Lambdaをキックする
3. ServiceWorkerでシリアル通信してパトランプする
でイケそうです。
# Web Push
大体上記の参考URLどおりで通知は受け取れると思います。
今回は相手が一人なので色々乱暴にやりました（ごめんなさい）
ポイントは

- generateVAPIDKeysを一度叩いてメモっておき
- プッシュの購読やServiceWorkerはlocalhostで登録（参考URLのvapid_demo.jsの辺り）

辺りでしょうか。
Lambdaのスクリプトはこんな感じです。

```js:handler.js
'use strict';

const webPush = require('web-push');

webPush.setVapidDetails(
    'mailto:hoge@fuga.piyo', // 第一引数は'mailto:～'というフォーマットでないとだめらしい
    'xxx', // メモったVAPIDKeysのpublicKey key
    'yyy'  // VAPIDKeysのprivate key
);

module.exports.push = async (body, context) => {
  try {
    await webPush.sendNotification(body, JSON.stringify({
      title: 'Web Push！',
    }));
  } catch (err) {
    console.log(err);
  }
  return {
    statusCode: 200,
  };
}
```

API Gatewayを通してsubscriptionの中身をLambdaに投げると無事Web Pushが届きます。ヨシ！
# JSでシリアル通信する
調べてみるとWebUSBというのを使えばシリアル通信できるっぽい。
【参考URL】
[WebUSBことはじめ](https://qiita.com/Aruneko/items/aebb75feca5bed12fe32)

実装してみると確かにJSでパトランプが回ります（毎度汚くてごめんなさい）

```js:serial_demo.js
(async _ => {
  let device;
  try {
    device = await navigator.usb.requestDevice({ filters: [{
        vendorId: 0x2341,
        productId: 0x0043
    }]});

    const device = devices[0];
    await device.open();
    await device.selectConfiguration(1);
    await device.claimInterface(1);

    var result = await device.transferOut(4, Uint8Array.of(0x00, 0x00, 0xff, 0x00, 0xff, 0x00));
    console.log(result);
  } catch (err) {
      console.log(err);
  }
})();

btnSerialTestON.onclick = async evt => {
  try {
    const devices = await navigator.usb.getDevices();
    console.log(devices);
    
    const device = devices[0];
    await device.open();
    await device.selectConfiguration(1);
    await device.claimInterface(1);

    var result = await device.transferOut(4, Uint8Array.of(0x31, 0x0A));
    console.log(result);
  } catch (err) {
    // No device was selected.
  }
};
```

あとはServiceWorkerでシリアル通信する処理を呼び出してやれば良い、はずでした…
# Web PushからはWebUSBに触れない
そのまんまなのですが、ServiceWorkerの中ではnavigator.usbがnullになります。
このままじゃパトランプが回せない…せっかくプッシュは届くのに…
なんとかしてプッシュを契機にできないかと考えたところ、こんなことを思いつきました。
**Web Pushの通信自体をトリガーにすれば良いのでは…！**（良い子は真似しないでください）
# tcpdumpで監視
プッシュを受け取る時の通信をtcpdumpで監視すると、Web Pushは`f188.1e100.net`を含むホストや、名前解決できないホストの5228ポートから飛んでくることがわかりました。
ここまで来たらもう引き返せません。
プッシュの通信（ぽいもの全て）が来たらパトランプを回します。

```shell:fanfare.sh
#!/bin/sh

if [ ! -c /dev/cu.usb-xxxx ]; then
  exit;
fi

killall cat
cat /dev/cu.usb-xxxx &

echo "rootのパスワード" | sudo -S tcpdump -i en0 -Alvvp | awk '
BEGIN { run="echo 1 | tee /dev/cu.usb-xxxx; afplay /somewhere/fanfare.mp4; echo 0 | tee /dev/cu.usb-xxxx"; }
/(.*.5228|f188.1e100.net).*length (510|280)/ { print | run; close(run); }'
```

【参考URL】
[Filtering output of tcpdump and running script when string is found in realtime](https://unix.stackexchange.com/questions/311710/filtering-output-of-tcpdump-and-running-script-when-string-is-found-in-realtime)
ファンファーレっぽい音はafplayコマンドで流します。音と光が揃うと派手めで良きです。

　　　↓
　　　↓
　　　↓
　　　↓　光るので念の為ご注意を
　　　↓
　　　↓
　　　↓
　　　↓
　　　↓
　　　↓
　　　↓

![ezgif.com-crop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57613/eb1f1d96-35b2-ddbb-63a7-a7cdc4ae2587.gif)

# まとめ
良い子は真似しないでください。