変な家族写真  ー ポルターガイスト機能 ー

# はじめに
最近『変な家族写真』という家族写真専用のフォトフレームのプロトタイプを作って、Festaでプレゼンしたり、campfireに投稿したりしました。
プレゼンやcampfireでは技術的なことに全く触れていないので、qiitaでは技術的なこと書いてみたいと思います。

# その前に『変な家族写真』って何？

詳しくは[campfireページ](https://camp-fire.jp/projects/view/209941#menu)を見るのが早いのですが、簡単に説明させてください。
田舎から出てきて家族と離れて暮らす人向けに構想したプロダクトで、
専用のラインボットと連携して親に連絡していないということを感知するとフォトフレームにセットした写真に変なエフェクトをかけてお知らせしてくれるっていうやつです。

こうなったり
![hige.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415401/da2de0cc-5c94-2b44-62d6-ab620d78e870.gif)

こうなったりします。
![迫る.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415401/c981b596-418a-303c-6e0f-3475696292aa.gif)

※上記はサンプルのため高速で変化させています。実際は理想の連絡スパンを設定して（1月に1回とか）その間にじわじわと変化します。

さらにこのフォトフレームの最上級のリマインド機能としてポルターガイスト機能というのがあります。
エフェクトで写真が変になっているにもかかわらず連絡していないとフォトフレームがひとりでに倒れてお知らせしてくれるという意地でも気づかせようという機能です。

このような感じです。
[![](http://img.youtube.com/vi/5LXTmDHlSR8/0.jpg)](http://www.youtube.com/watch?v=5LXTmDHlSR8)


# ポルターガイスト機能仕組み

今回は、このポルターガイスト機能の仕組みについて書こうと思います。
その前に、前提なのですが、これプロトタイプとして制作していて、とりあえずこれが使えるのかっていうのを検証するために表側に見える機能しか実装していません。
なので、フォトフレームと利用者のラインのアカウントの紐付けどうするとかっていうのは実装されてません。。あしからず。

**概念図**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415401/f0e4a2ac-ab56-4979-bce8-b506a1af67e9.png)

## ポルターガイスト流れ

1. 家族間のやりとりはラインボットで見とく
2. 一定期間やりとりがないとボットサーバーからwebsocket経由でフォトフレーム（ブラウザ)へエフェクトスタートをトリガーする
3. エフェクト完了後はフォトフレームからボットサーバーへエフェクト完了をwebsocket経由で通知
4. ボットサーバーからフォトフレームのobniz起動　距離センサスタート
5. 人が70cm以内の距離に近づくとサーボを動かしてフォトフレームを倒す

### ブラウザ側(分割しているのを集約しています)
```js 

const animHandler = new AnimHandler({
  onstart() {
    photo.classList.add("is-old")
  },
  oncompleat() {
    // エフェクト完了後
    startMeasure()
  },
  onupdate({ main, effect, sepia, brightness, saturate, scale, fire }) {
    canvasCtr.crossFade(
      { main, effect },
      `sepia(${sepia}) brightness(${brightness}) saturate(${saturate})`,
      scale,
      fire
    )
  },
  onRepairStart() {
    photo.classList.remove("is-old")
  },
  duration: minuteToMilli(5)
})

//以下別ファイルの記述
//startMeasure
export function startMeasure() {
  socket.emit("startMeasure")
}
```

### ボットサーバー側
ボットサーバーの設定などは省略しています。

```js
function handleMeasure() {
  obnizHandler.start(onFall, onRaise)
}

function addSocketsHandler(socket, id) {
  socket.on("disconnect", () => {
    handleDisConnect(id)
  })
  socket.on("shareimg", handleShareImg)
　// 距離センサスタート
  socket.on("startMeasure", handleMeasure)
}

io.on("connection", socket => {
  const id = socket.id
  sockets.set(id, socket)
  console.log("connect", socket.id)
  addSocketsHandler(socket, id)
})

//以下別ファイル
const Obniz = require("obniz")
const distancethreshold = 800
const angle = {
  default: 110,
  fall: 180
}

const devices = {
  servo: {
    name: "ServoMotor",
    pin: { gnd: 0, vcc: 1, signal: 2 } 
  },
  hcsr04: {
    name: "HC-SR04",
    pin: { gnd: 7, echo: 6, trigger: 5, vcc: 4 }
  }
}

const conditions = {
  fall(distance) {
    return distance <= distancethreshold
  },
  fallup(distance) {
    return distance && distance >= distancethreshold
  }
}

function sleep(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms)
  })
}

//Obnizのハンドラ
class ObnizHandler{
  constructor(id) {
    this.id = id
    this.obniz = new Obniz(this.id)
    this.devices = null
  }

  connect() {
    return new Promise(resolve => {
      this.obniz.onconnect = () => {
        this.initDevices()
        resolve()
      }
    })
  }

  initDevices() {
    this.devices = Object.keys(devices).reduce((res, key) => {
      const {pin, name} = devices[key]
      res[key] = this.obniz.wired(name, pin)
      return res
    }, {})
  }

  async start(fallCb, fallUpCb) {
    const servo = this.devices.servo
    //倒れる
    await this.measure(conditions.fall)
    servo.on()
    servo.angle(angle.fall)
    if (fallCb) fallCb()

    //サーボの角度を戻す
    await sleep(1000)
    servo.angle(angle.default)
    await sleep(1000)
    servo.off()

    //起こされたか判定
    await this.measure(conditions.fallup)
    if (fallUpCb) fallUpCb()
  }

  async measure(stopCondition) {
    let shouldMeasure = true
    while (shouldMeasure) {
      const distance = await this.devices.hcsr04.measureWait()
      console.log(distance)
      if (stopCondition(distance)) break
    }
  }
}
```

#最後に
ご興味あり方は[campfireページ](https://camp-fire.jp/projects/view/209941#menu)見てってくださいねー

明日は@mihoko-funatsuさんですー！