あらすじ
IoTLTで下記の発表をした。1万人に1人ぐらいは同じ事したいひとは居ると思うので、デモで使ったものを作る手順を公開したい。
結果
IoTLTで使ったスライド(スライド埋め込みが聞いてない場合はリンクから開いてください)
~~スライド埋め込みの方法分かんない。~~埋め込み方法教えていただきました。ありがとうございます!
全体構成
ざっくりと作ったものを説明すると、顔を検知したら光って音鳴らす機械
。
- 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をアップデートする。
公式を参考に最新版のファームを入れる。
まずはダウンロード | 子供パソコン IchigoJamから最新版ファームをダウンロードする。とりあえず1.3.1をダウンロードした。
次にHOME of IJUtilitiesから書き込み用ツールをダウンロードする。
IJUtilities.exeを起動するとフォントがどうとかで怒られるの
一緒にエクスプローラが開くので、Font\for IJ1.2\IchigoJam-for-Display-1.2.ttfを選択してインストールする。
フォントインストール完了後にcontinueを済ませると下記の様な画面が開く
IchigoJamをUSBケーブルで接続し、
10分ほど使い方を悩んだのち、公式に書いてある通りにする。
IJUtilities を起動した後、
ターミナルセンター ウインドウの
ツールバー オプション - Firm書換え (IchigoJam, PanCake...)
を選択して下さい。lpc21isp ウインドウが表示されます。
何も出てきてくれない。
とりあえずココのUSBドライバ入れてみる。
状況変わらず
どうにもならなそうなのでIchigoJamは封印することにしてラズパイでやる。
MAiX BiTで人を認識してGPIO出力する
MAiX BiTでダックを検出する予定だったけど無理だったを元に顔の検出用ファームを焼きこむ。
でdemo_find_face.pyをコピペしてみると
Traceback (most recent call last):
File "<stdin>", line 10
SyntaxError: invalid syntax
何コレ?もしかしてMAiXBiTも動かないのか?
1行ずつ叩いてみると
>>> 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番ピンで顔認識を出力するように変更する。
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 | 顔認識中 |
とりあえず必要なパッケージ入れる。
pip3 install RPi.GPIO
pip3 install flask
で、書く。
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へ送信
上記点のみこなせれば後はどうでもいい。
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つ
- 回路図は気が向いたら乗せる。
こんな感じ
変換は12[v]->3[v]。12[v]->5[v]->3[v]の方が良いらしいけどやってない。
制御基板
- ハロウィンライト、警報機はフォトカプラ + NPNトランジスタを利用
- パトランプに関してはフォトカプラ + NPNトランジスタ + リレー
- 回路図は気が向いたら乗せる。
こんな感じ
万が一用に手動でも動かせるようにしておいた。
ふりかえり
シータ役の方に落ちてきた位置を調整してもらいつつ、落ちてきた女の子(実際は横移動の男の子)を認識できた。
やる事やれて嬉しい。
親方空から女の子が! #親方空から女の子が
— みやた@代打中 (@miyata080825) September 27, 2019