親方空から女の子がをするための準備をする

# あらすじ

IoTLTで下記の発表をした。1万人に1人ぐらいは同じ事したいひとは居ると思うので、デモで使ったものを作る手順を公開したい。

# 結果

IoTLTで使ったスライド(スライド埋め込みが聞いてない場合はリンクから開いてください)

<div style="width: 60%;">
<script async class="speakerdeck-embed" data-slide="1" data-id="8768dc73ed6e42ff89c5fe2a35cf163c" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>
</div>

[親方空から女の子が](https://speakerdeck.com/miyatanam/the-girl-fall-in-down-from-sky-high)

<font color="dark_pink">~~スライド埋め込みの方法分かんない。~~埋め込み方法教えていただきました。ありがとうございます!</font>



# 全体構成

ざっくりと作ったものを説明すると、`顔を検知したら光って音鳴らす機械`。

 + MAiXBit
   + 顔の検知
     + 顔を検知したらGPIO経由でラズパイに通知
 + RaspberryPi
   + 音・光をアクチュエータ制御基板経由で動かす
     + パトライト: 12[v]
     + 音: 3.3[v]
     + 光: 3.3[v]
 + AndroidApp
   + 音声を受け取りラズパイに起動・状態遷移を通知
   + アプリ側は3秒に一回ラズパイから状態を取得
 + 電力回り
   + 12[v] バッテリー
   + ブレーカー + 10[A]ヒューズ
   + 電力供給基盤
     + 三端子レギュレータで5[v],3[v]を作成
     + 12[v]はそのまま通過
   + アクチュエータ制御基板
     + フォトカプラ + NPNトランジスタで電力供給

# IchigoJamを使おうとしてあきらめる

その昔mruby用に使ってしまったため、Basicが使えない状態になっている。
初期化もかねてFirmをアップデートする。

[公式](https://15jamrecipe.jimdo.com/%E5%91%A8%E8%BE%BA%E6%A9%9F%E5%99%A8/%E3%83%91%E3%82%BD%E3%82%B3%E3%83%B3%E3%81%A8%E6%8E%A5%E7%B6%9A/%E3%83%95%E3%82%A1%E3%83%BC%E3%83%A0%E3%82%A6%E3%82%A7%E3%82%A2%E6%9B%B4%E6%96%B0/)を参考に最新版のファームを入れる。

まずは[ダウンロード | 子供パソコン IchigoJam](https://ichigojam.net/download.html)から最新版ファームをダウンロードする。とりあえず1.3.1をダウンロードした。

次に[HOME of IJUtilities](http://ijutilities.micutil.com/#)から書き込み用ツールをダウンロードする。

IJUtilities.exeを起動するとフォントがどうとかで怒られるの

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/c26f7f52-15b7-f4f2-dd62-1cfdc3202a64.png)

一緒にエクスプローラが開くので、Font\for IJ1.2\IchigoJam-for-Display-1.2.ttfを選択してインストールする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/08fe3a7e-c176-640d-a1cb-f3b956f56a8c.png)

フォントインストール完了後にcontinueを済ませると下記の様な画面が開く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/83f969a6-8a46-927b-d2e2-60680877c46b.png)

IchigoJamをUSBケーブルで接続し、
10分ほど使い方を悩んだのち、公式に書いてある通りにする。

```text
IJUtilities を起動した後、
ターミナルセンター ウインドウの
ツールバー オプション - Firm書換え  (IchigoJam, PanCake...)
を選択して下さい。lpc21isp ウインドウが表示されます。
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/3e6b9141-410c-6033-94be-a27883d835c5.png)

<font color="dark_pink">何も出てきてくれない。</font>

とりあえず[ココ](https://jp.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers)のUSBドライバ入れてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/413d50d8-b93c-4188-7b2d-aa71847a3e38.png)

<font color="dark_pink">状況変わらず</font>

どうにもならなそうなのでIchigoJamは封印することにしてラズパイでやる。

# MAiX BiTで人を認識してGPIO出力する

[MAiX BiTでダックを検出する予定だったけど無理だった](https://qiita.com/miyatama/items/5d771fb016c8574e47f4)を元に顔の検出用ファームを焼きこむ。

で[demo_find_face.py](https://github.com/sipeed/MaixPy_scripts/blob/master/machine_vision/demo_find_face.py)をコピペしてみると

```text
Traceback (most recent call last):
  File "<stdin>", line 10
SyntaxError: invalid syntax
```

何コレ？もしかしてMAiXBiTも動かないのか？
1行ずつ叩いてみると

```text
>>> task = kpu.load(0x300000)
v=3, flag=1, arch=0, layer len=24, mem=45000, out cnt=1
model_size=388776
E (236951065342) SYSCALL: Out of memory
```

なぜだかはよく分からないが、uPyLoader経由でboot.py上書きするとうまくいった。

とりあえず6番ピンで顔認識を出力するように変更する。

```python
import sensor
import image
import lcd
import KPU as kpu
from Maix import GPIO
from board import board_info
from fpioa_manager import fm

fm.register(6, fm.fpioa.GPIO0)
facedetect_signal = GPIO(GPIO.GPIO0, GPIO.OUT)

lcd.init()
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.run(1)
task = kpu.load(0x300000)
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
a = kpu.init_yolo2(task, 0.5, 0.3, 5, anchor)
facedetect_signal.value(0)
while(True):
    img = sensor.snapshot()
    code = kpu.run_yolo2(task, img)

    if code:
        facedetect_signal.value(1)
        for i in code:
            print(i)
            a = img.draw_rectangle(i.rect())
    else:
      facedetect_signal.value(0)
    a = lcd.display(img)
a = kpu.deinit(task)
fm.unregister(6, fm.fpioa.GPIO0)
```

# ラズパイをVUIとGPIOに対応させる

取り合えず欲しい機能は下記

 + アクチュエータ制御
   + 起動したらハロウィン起動
   + 顔認識モードになったらしたらパトランプ起動
   + 顔認識したらサウンド起動
 + AndroidアプリとのIF
   + 起動の合図を受け付ける
     + post: state
   + 状態確認
     + get: state

状態としては下記を考える。1->2->3->4->2の順で遷移する。例外は認めない。

| no | name | description | 
| ----: | :----- | :------ |
| 1 | init | 起動直後 |
| 2 | wait_voice_detect | 音声入力待ち |
| 3 | wait_face_detect | 顔認識待ち | 
| 4 | face_detecting | 顔認識中 |

とりあえず必要なパッケージ入れる。

```bash
pip3 install RPi.GPIO
pip3 install flask
```

で、書く。

```python:main.py
import RPi.GPIO as GPIO
import time
import threading
from flask import Flask, render_template, request, redirect, url_for, jsonify

app = Flask(__name__)

state = 0
STATUS_NONE = 0 
STATUS_INIT = 1
STATUS_WAIT_VOICE_DETECT = 2
STATUS_WAIT_FACE_DETECT = 3
STATUS_FACE_DETECT = 4
facedetect_pinno = 5
actuator01_pinno = 10
actuator02_pinno = 17
actuator03_pinno = 26

def main():
  global state
  global facedetect_pinno

  change_state(STATUS_INIT)

  GPIO.setmode(GPIO.BCM)
  GPIO.setup(facedetect_pinno, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
  GPIO.setup(actuator01_pinno, GPIO.OUT)
  GPIO.setup(actuator02_pinno, GPIO.OUT)
  GPIO.setup(actuator03_pinno, GPIO.OUT)
  try:
    GPIO.output(actuator01_pinno, 0)
    GPIO.output(actuator02_pinno, 0)
    GPIO.output(actuator03_pinno, 0)
  
    change_state(STATUS_WAIT_VOICE_DETECT)
    while True:
      time.sleep(1)
      if state == STATUS_WAIT_VOICE_DETECT:
        continue
      if state == STATUS_WAIT_FACE_DETECT:
        if GPIO.input(facedetect_pinno) == GPIO.HIGH:
          change_state(STATUS_FACE_DETECT)
        continue
      if state == STATUS_FACE_DETECT:
        time.sleep(30)
        change_state(STATUS_WAIT_VOICE_DETECT)
        continue
  finally:
    # TODO expect KeyboardInterrupt
    GPIO.output(actuator01_pinno, 0)
    GPIO.output(actuator02_pinno, 0)
    GPIO.output(actuator03_pinno, 0)
    GPIO.cleanup()

@app.route('/state', methods=['GET'])
def get_state():
  global state
  output = {
    "state":state
  }
  return jsonify(output)

@app.route('/state', methods=['POST'])
def post_state():
  global state
  next_state = request.json['state']
  change_state(next_state) 
  output = {
    "state":state
  }
  return jsonify(output)

def change_state(next_state):
  global state
  if state == STATUS_NONE and next_state == STATUS_INIT:
    print("change state: INIT")
    state = next_state
    return

  if state == STATUS_INIT and next_state == STATUS_WAIT_VOICE_DETECT:
    print("change state: WAIT_VOICE_DETECT")
    GPIO.output(actuator01_pinno, 1)
    state = next_state
    return

  if state == STATUS_WAIT_VOICE_DETECT and next_state == STATUS_WAIT_FACE_DETECT:
    print("change state: WAIT_FACE_DETECT")
    GPIO.output(actuator02_pinno, 1)
    state = next_state
    return

  if state == STATUS_WAIT_FACE_DETECT and next_state == STATUS_FACE_DETECT:
    print("change state: FACE_DETECT")
    GPIO.output(actuator03_pinno, 1)
    state = next_state
    return

  if state == STATUS_FACE_DETECT and next_state == STATUS_WAIT_VOICE_DETECT:
    print("change state: WAIT_VOICE_DETECT")
    GPIO.output(actuator02_pinno, 0)
    GPIO.output(actuator03_pinno, 0)
    state = next_state
    return

  print("change state error {0} to {1}".format(state, next_state))

if __name__ == "__main__":
  mainThread = threading.Thread(target=main)
  mainThread.start()
  app.debug = True
  app.run(host='0.0.0.0', port='3000')
```

# Android側のアプリを作る

Android側で行うのは下記点

 + RaspberryPiから現在の状態を取得して画面に表示
 + システム起動を受け取りRaspberryPiへ送信

上記点のみこなせれば後はどうでもいい。

```kotlin
package com.example.thegirlfalldown

import android.app.Notification
import android.content.Intent
import android.net.Uri
import android.os.*
import androidx.appcompat.app.AppCompatActivity
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.util.Log
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.core.os.HandlerCompat.postDelayed
import kotlinx.android.synthetic.main.activity_main.*

import org.json.JSONArray
import org.json.JSONObject
import java.io.*
import java.net.HttpURLConnection
import java.net.URL


class MainActivity : AppCompatActivity() {
    val TAG = MainActivity::class.java.simpleName
    lateinit var handler: Handler
    lateinit var lblStatus: TextView
    lateinit var txtRaspiIPAddr: EditText
    var MSG_DETECT_STARTUP_SYSTEM = 1001
    var MSG_STATUS_REFRESH = 1002

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        lblStatus = findViewById(R.id.lblStatus)
        txtRaspiIPAddr = findViewById(R.id.txtRaspiIPAddr)
        handler = Handler(object: Handler.Callback {
            override fun handleMessage(msg: Message): Boolean {
                if (msg.what == MSG_DETECT_STARTUP_SYSTEM){
                    val task = object: AsyncTask<String, String, String>() {
                        override fun doInBackground(vararg param: String?): String {

                            var result:String = ""

                            val url = URL(param[0])
                            val httpClient = url.openConnection() as HttpURLConnection
                            httpClient.setReadTimeout(3000)
                            httpClient.setConnectTimeout(3000)
                            httpClient.requestMethod = param[1]

                            httpClient.instanceFollowRedirects = false
                            httpClient.doOutput = true
                            httpClient.doInput = true
                            httpClient.useCaches = false
                            httpClient.setRequestProperty("Content-Type", "application/json; charset=utf-8")
                            try {
                                httpClient.connect()
                                val os = httpClient.getOutputStream()
                                val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8"))
                                writer.write(param[1])
                                writer.flush()
                                writer.close()
                                os.close()

                                if (httpClient.responseCode == HttpURLConnection.HTTP_OK) {
                                    val stream = BufferedInputStream(httpClient.inputStream)
                                    val data: String = readStream(inputStream = stream)
                                    return data
                                } else {
                                    println("ERROR ${httpClient.responseCode}")
                                }
                            } catch (e: Exception) {
                                e.printStackTrace()
                            } finally {
                                httpClient.disconnect()
                            }

                            return ""
                        }

                        fun readStream(inputStream: BufferedInputStream): String {
                            val bufferedReader = BufferedReader(InputStreamReader(inputStream))
                            val stringBuilder = StringBuilder()
                            bufferedReader.forEachLine { stringBuilder.append(it) }
                            return stringBuilder.toString()
                        }

                        override fun onPostExecute(result: String?) {
                            var status = ""
                            if ( result != ""){
                                val toast = Toast.makeText(applicationContext, result, Toast.LENGTH_LONG)
                                toast.show()
                            }
                        }
                    }
                    if ("${txtRaspiIPAddr.text}" != "") {
                        val addr = "http://${txtRaspiIPAddr.text}:3000/state"
                        val poststring =  "{\"status\": 3}"
                        task.execute(addr, poststring)
                    }else {
                        val toast = Toast.makeText(applicationContext, "アドレス入れろ", Toast.LENGTH_LONG)
                        toast.show()
                    }
                    return true
                }
                if (msg.what == MSG_STATUS_REFRESH) {
                    val task = object: AsyncTask<String, String, Long>() {
                        override fun doInBackground(vararg param: String?): Long {

                            var result:Long = 0L

                            val url = URL(param[0])
                            val httpClient = url.openConnection() as HttpURLConnection
                            httpClient.setReadTimeout(3000)
                            httpClient.setConnectTimeout(3000)
                            httpClient.requestMethod = "GET"

                            try {
                                if (httpClient.responseCode == HttpURLConnection.HTTP_OK) {
                                    val stream = BufferedInputStream(httpClient.inputStream)
                                    val data: String = readStream(inputStream = stream)
                                    val returnData = JSONObject(data)
                                    if (returnData.has("status")) {
                                        result = returnData.getLong("status")
                                    }
                                } else {
                                    println("ERROR ${httpClient.responseCode}")
                                }
                            } catch (e: Exception) {
                                e.printStackTrace()
                            } finally {
                                httpClient.disconnect()
                            }

                            return result
                        }

                        fun readStream(inputStream: BufferedInputStream): String {
                            val bufferedReader = BufferedReader(InputStreamReader(inputStream))
                            val stringBuilder = StringBuilder()
                            bufferedReader.forEachLine { stringBuilder.append(it) }
                            return stringBuilder.toString()
                        }
                        override fun onPostExecute(result: Long?) {
                            var status = ""
                            if ( result == 0L){
                                status = "Status: Unknown"
                            }
                            if (result == 1L ){
                                status = "Status: Init"
                            }
                            if (result == 2L) {
                                status = "Status: Wait Voice Detect"
                            }
                            if (result == 3L ) {
                                status = "Status: Wait Face Detect"
                            }
                            if (result == 4L ){
                                status = "Status: Face Detecting"
                                tweetGirlFallDown()
                            }
                            lblStatus.text = status
                        }
                    }
                    if ("${txtRaspiIPAddr.text}" != "") {
                        val addr = "http://${txtRaspiIPAddr.text}:3000/state"
                        task.execute(addr)
                    }
                }
                return false
            }
        })

        val timerHandler = Handler()
        val r = object : Runnable {
            internal var count = 0
            override fun run() {
                val msg = Message.obtain()
                msg.what = MSG_STATUS_REFRESH
                handler.sendMessage(msg)
                timerHandler.postDelayed(this, 3000)
            }
        }
        timerHandler.post(r)
    }

    fun tweetGirlFallDown() {
        val intent = Intent (Intent.ACTION_VIEW);
        val messsage = Uri.encode("親方空から女の子が！ #親方空から女の子がシステム");
        intent.setData(Uri.parse("twitter://post?message=" + messsage));
        startActivity(intent);
    }

    fun onClickBtnVoiceDetect(v: View) {
        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
        intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, true)
        intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, packageName)
        val recognizer = SpeechRecognizer.createSpeechRecognizer(applicationContext)
        recognizer.setRecognitionListener(object : RecognitionListener {
            override fun onReadyForSpeech(p0: Bundle?) {}
            override fun onRmsChanged(p0: Float) {}
            override fun onBufferReceived(p0: ByteArray?) {}
            override fun onPartialResults(p0: Bundle?) {}
            override fun onEvent(p0: Int, p1: Bundle?) {}
            override fun onBeginningOfSpeech() {}
            override fun onEndOfSpeech() {}
            override fun onError(p0: Int) {}
            override fun onResults(results: Bundle?) {
                if (results == null) {
                    return
                }
                val texts = results!!.getStringArrayList(android.speech.SpeechRecognizer.RESULTS_RECOGNITION)
                var detectKeyword = false
                texts?.let {
                    for (text in texts) {
                        Log.w(TAG, "recognize: $text")
                        if (text == "起動開始") {
                            detectKeyword = true
                            break
                        }
                    }
                    if (!detectKeyword) {
                        val msg = Message.obtain()
                        msg.what = MSG_DETECT_STARTUP_SYSTEM
                        handler.sendMessage(msg)
                    }
                }
            }
        })
        recognizer.startListening(intent)
    }
}
```

# ハードを組む

今回動かすもの整理

| no | name | voltage[v] | description |
| -----: | :----- | -----: | :----- |
| 1 | MAiX BiT | 5 | |
| 2 | Raspberry Pi 3B | 5 | |
| 3 | ハロウィンライト | 3 | ラズパイから制御 |
| 4 | 警報機 | 3 | ラズパイから制御 |
| 5 | パトランプ | 12 | ラズパイから制御。リレー経由で動かす |
| 6 | 車用パトランプ | 12 | パトランプと同一タイミングで動かしたかったけど入手間に合わず。 |

電圧変換基盤

 + 12[v]はバッテリーからとる
   + バッテリーで電源切り離せるようにする
   + 10[A]のヒューズ入れる
 + 12[v]から3[v],5[v]を三端子レギュレータで作成する。
 + 入出力 
   + 入力は12[v] 
   + 3[v]出力の口が3つ
   + 5[v]出力の口が2つ
   + 12[v]出力の口が2つ
 + 回路図は気が向いたら乗せる。
 
こんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/e5e1311a-d0b8-04e0-888d-03448b8f734e.png)

<font color="dark_pink">変換は12[v]->3[v]。12[v]->5[v]->3[v]の方が良いらしいけどやってない。</font>

制御基板

 + ハロウィンライト、警報機はフォトカプラ + NPNトランジスタを利用
 + パトランプに関してはフォトカプラ + NPNトランジスタ + リレー
 + 回路図は気が向いたら乗せる。

こんな感じ 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/43fc7308-6ffd-f092-0318-6c9b3be3d1db.png)

<font color="dark_pink">万が一用に手動でも動かせるようにしておいた。</font>


# ふりかえり

シータ役の方に落ちてきた位置を調整してもらいつつ、落ちてきた女の子(実際は横移動の男の子)を認識できた。
やる事やれて嬉しい。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">親方空から女の子が！ <a href="https://twitter.com/hashtag/%E8%A6%AA%E6%96%B9%E7%A9%BA%E3%81%8B%E3%82%89%E5%A5%B3%E3%81%AE%E5%AD%90%E3%81%8C?src=hash&amp;ref_src=twsrc%5Etfw">#親方空から女の子が</a></p>&mdash; みやた@代打中 (@miyata080825) <a href="https://twitter.com/miyata080825/status/1177374786843856897?ref_src=twsrc%5Etfw">September 27, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>