ブログ

ブログです

ただいまシステムの中身④ 全体プログラム

長々と4回にもわたって続けてきたこの話だけど、いよいよ全体像を出す時が来た。
何はともあれまずは全体プログラムを出そう。

ただいまシステム

メインプログラム

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
import xml.etree.ElementTree as ET
import wiringpi2 as wp
import RPi.GPIO as GPIO
import time
import sys
import threading
import pyping
import random
import pygame.mixer
from datetime import datetime
from wakeonlan import wol

# 各種状態保持用変数
room_state = 1
corridor_state = 0
alive = 0
atHome = 1
allowed = 0
counter = 0

# GPIO pin setup
servo_pin_room = 13
servo_pin_corridor = 12
state_pin_room = 2
state_pin_corridor = 3

# RPIO setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(20, GPIO.IN, GPIO.PUD_DOWN)
GPIO.setup(21, GPIO.IN, GPIO.PUD_DOWN)

# Wiring Pi setup
wp.wiringPiSetupGpio()
wp.pinMode(servo_pin_room, 2)
wp.pinMode(servo_pin_corridor, 2)
wp.pinMode(state_pin_room, 1)
wp.pinMode(state_pin_corridor, 1)
wp.pwmSetMode(0)
wp.pwmSetRange(1024)
wp.pwmSetClock(375)

room_on = int((4.75*35/90 + 7.25)*(1024/100))
corridor_off = int((4.75*5/90 + 7.25)*(1024/100))

wp.pwmWrite(servo_pin_room, room_on)
wp.pwmWrite(servo_pin_corridor, corridor_off)

wp.digitalWrite(state_pin_room, room_state)
wp.digitalWrite(state_pin_corridor, corridor_state)

# 返答音声
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=1024)
haihai = pygame.mixer.Sound("/home/pi/python_code/haihai_02.wav")
haai = pygame.mixer.Sound("/home/pi/python_code/haai_02.wav")
makasete = pygame.mixer.Sound("/home/pi/python_code/makasete_01.wav")
ok = pygame.mixer.Sound("/home/pi/python_code/ok_01.wav")
ryoukai = pygame.mixer.Sound("/home/pi/python_code/ryoukaidesu_02.wav")
itterasshai = pygame.mixer.Sound("/home/pi/python_code/itterasshai_02.wav")
misu = pygame.mixer.Sound("/home/pi/python_code/misumisu_01.wav")
oha = pygame.mixer.Sound("/home/pi/python_code/ohayou_02.wav")
okaeri = pygame.mixer.Sound("/home/pi/python_code/okaeri_03.wav")
oyasumi = pygame.mixer.Sound("/home/pi/python_code/oyasuminasai_04.wav")

# サーボ動作用関数
def moveServo(set_degree, place):
    if place == 'room':
        move_deg = int((4.75*set_degree/90 + 7.25)*(1024/100))
        wp.pwmWrite(servo_pin_room, move_deg)
        print 'room'
    elif place == 'corridor':
        move_deg = int((4.75*set_degree/90 + 7.25)*(1024/100))
        wp.pwmWrite(servo_pin_corridor, move_deg)
        print 'corridor'

# 物理スイッチ用割り込み関数①
def intFuncRoom(channel):
    global room_state
    moveServo(10*room_state - 35*(room_state-1), 'room')
    room_state = not(room_state)
    wp.digitalWrite(state_pin_room, room_state)

# 物理スイッチ用割り込み関数②
def intFuncCorridor(channel):
    global corridor_state
    moveServo(5*corridor_state - 30*(corridor_state-1), 'corridor')
    corridor_state = not(corridor_state)
    wp.digitalWrite(state_pin_corridor, corridor_state)

# 在室確認用関数
def keepAlive():
    global alive
    global atHome
    global counter

    host_1 = "192.168.0.2"
    host_2 = "192.168.0.5"

    RPing_1 = pyping.ping(host_1)
    RPing_2 = pyping.ping(host_2)

    if (RPing_1.ret_code == 0) | (RPing_2.ret_code == 0):
        alive = 1
        if not(atHome) and counter > 10:
            okaeri.play()
            moveServo(35, 'room')
            moveServo(30, 'corridor')
            room_state = 1
            corridor_state = 1
            atHome = 1
        counter = 0
    else:
        alive = 0
        if counter < 100:
            counter += 1

    t = threading.Timer(1, keepAlive)
    t.start()

# サーボ状態管理用関数
def stateManage():
    global room_state
    global corridor_state

    room_state = wp.digitalRead(state_pin_room)
    corridor_state = wp.digitalRead(state_pin_corridor)

    t = threading.Timer(1, stateManage)
    t.start()

# 命令受付管理タイマ
def allowedTimer():
    global allowed

    if allowed:
        allowed = not(allowed)
        misu.play()

    print('Timer expired. allowed = ' + str(allowed))

# 返答音声再生用関数
def response(scene):
    if scene == 'hi':
        n = random.randint(1, 2)
        print n
        if n == 1:
            haai.play()
        elif n == 2:
            haihai.play()
    elif scene == 'ok':
        n = random.randint(1, 3)
        print n
        if n == 1:
            makasete.play()
        elif n == 2:
            ok.play()
        elif n == 3:
            ryoukai.play()
    elif scene == 'home':
        okaeri.play()
    elif scene == 'out':
        itterasshai.play()
    elif scene == 'morning':
        oha.play()
    elif scene == 'night':
        oyasumi.play()

# メイン関数
def main():
    global room_state
    global corridor_state
    global alive
    global atHome
    global allowed
    isSleep = 0

    host = 'localhost'
    port = 10500

    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((host, port))

    GPIO.add_event_detect(20, GPIO.RISING, callback=intFuncCorridor, bouncetime=300)
    GPIO.add_event_detect(21, GPIO.RISING, callback=intFuncRoom, bouncetime=300)

    try:
        data = ''
        while 1:
            if '</RECOGOUT>\n.' in data:
                root = ET.fromstring('<?xml version="1.0"?>\n' + data[data.find('<RECOGOUT>'):].replace('\n.', ''))
                for whypo in root.findall('./SHYPO/WHYPO'):
                    command = whypo.get('WORD')
                    score = float(whypo.get('CM'))
                    print command
                    print score

                    hour = datetime.now().hour
                    if 1 < hour < 5:
                        dndsturb = 0
                    else:
                        dndisturb = 1

                    if command == u'パキン' and score >= 0.96 and not(allowed) and alive:
                        allowed = 1
                        print('allowed = ' + str(allowed))
                        response('hi')
                        timer = threading.Timer(5, allowedTimer)
                        timer.start()

                    if alive & dndisturb:
                        if command == u'ただいま' and score >= 0.7 and not(atHome):
                            atHome = 1
                            response('home')
                            moveServo(35, 'room')
                            moveServo(30, 'corridor')
                            room_state = 1
                            corridor_state = 1
                            allowed = 0
                        elif command == u'おやすみ' and score >= 0.7 and allowed:
                            isSleep = 1
                            response('night')
                            moveServo(10, 'room')
                            moveServo(5, 'corridor')
                            room_state = 0
                            corridor_state = 0
                            allowed = 0
                        elif command == u'いってきます' and score >= 0.7 and atHome and allowed:
                            atHome = 0
                            response('out')
                            moveServo(10, 'room')
                            moveServo(5, 'corridor')
                            room_state = 0
                            corridor_state = 0
                        elif command == u'部屋つけて' and score >= 0.7 and atHome and allowed:
                            response('ok')
                            moveServo(35, 'room')
                            room_state = 1
                            allowed = 0
                        elif command == u'おはよう' and score >= 0.7 and isSleep:
                            isSleep = 0
                            response('morning')
                            moveServo(35, 'room')
                            room_state = 1
                        elif command == u'部屋消して' and score >= 0.7 and atHome and allowed:
                            response('ok')
                            moveServo(10, 'room')
                            room_state = 0
                            allowed = 0
                        elif command == u'廊下つけて' and score >= 0.7 and atHome and allowed:
                            response('ok')
                            moveServo(30, 'corridor')
                            corridor_state = 1
                            allowed = 0
                        elif command == u'廊下消して' and score >= 0.7 and atHome and allowed:
                            response('ok')
                            moveServo(5, 'corridor')
                            corridor_state = 0
                            allowed = 0
                        elif command == u'パソコンつけて' and score >= 0.8 and atHome and allowed:
                            response('ok')
                            wol.send_magic_packet('xx.xx.xx.xx.xx.xx')
                            allowed = 0

                    wp.digitalWrite(state_pin_room, room_state)
                    wp.digitalWrite(state_pin_corridor, corridor_state)

                data = ''
            else:
                data = data + client.recv(1024)
    except KeyboardInterrupt:
        client.close()
        GPIO.cleanup()

if __name__ == "__main__":
    t1 = threading.Thread(target=keepAlive)
    t1.setDaemon(True)
    t1.start()

    t2 = threading.Thread(target=stateManage)
    t2.setDaemon(True)
    t2.start()

    time.sleep(1)

    main()

タッチパネル制御関数

#!/usr/bin/python
# -*- coding: utf-8 -*-
import webiopi
import wiringpi2 as wp

#webiopi.setDebug()

# GPIO setup
servo_pin_room = 13
servo_pin_corridor = 12
state_pin_room = 2
state_pin_corridor = 3

wp.wiringPiSetupGpio()
wp.pinMode(servo_pin_room, 2)
wp.pinMode(servo_pin_corridor, 2)
wp.pinMode(state_pin_room, 1)
wp.pinMode(state_pin_corridor, 1)
wp.pwmSetMode(0)
wp.pwmSetRange(1024)
wp.pwmSetClock(375)

def moveServo(set_degree, place):
    if place == 'room':
        move_deg = int((4.75*set_degree/90 + 7.25)*(1024/100))
        wp.pwmWrite(servo_pin_room, move_deg)
    elif place == 'corridor':
        move_deg = int((4.75*set_degree/90 + 7.25)*(1024/100))
        wp.pwmWrite(servo_pin_corridor, move_deg)

# setup function is automatically called at WebIOPi startup
def setup():
    room_state = wp.digitalRead(state_pin_room)
    corridor_state = wp.digitalRead(state_pin_corridor)

# loop function is repeatedly called by WebIOPi 
def loop():
    # gives CPU some time before looping again
    room_state = wp.digitalRead(state_pin_room)
    corridor_state = wp.digitalRead(state_pin_corridor)
    webiopi.sleep(1)

@webiopi.macro
def roomButton():
    room_state = wp.digitalRead(state_pin_room)
    moveServo(10*room_state - 35*(room_state-1), 'room')
    room_state = not(room_state)
    wp.digitalWrite(state_pin_room, room_state)

@webiopi.macro
def corridorButton():
    corridor_state = wp.digitalRead(state_pin_corridor)
    moveServo(5*corridor_state - 30*(corridor_state-1), 'corridor')
    corridor_state = not(corridor_state)
    wp.digitalWrite(state_pin_corridor, corridor_state)

「なんかこれまでの説明に全く出てきてない機能多くない?」とか「タッチパネル制御関数ってなに?」とかいろいろ突っ込みどころはあると思うけど、作っていくうちにいろいろ不満点が見えてきたり、こんな機能があったほうが良いよなぁって思ったりで、今までまったく説明に出てこなかった機能が大量に入っちゃった。
そこら辺も含めて適当に説明してきます。

システムブロック図

現時点でのシステム全体像は下図のようになってる。
f:id:hira-hide:20170709120440p:plain
サーボ操作用の系統は全部で3つあって、音声操作がJulius+Wiring Pi物理スイッチがRPIO、そんでタッチパネルがWebIOPi。本当は物理スイッチの制御もWiring Piでやりたかったんだけど、割り込み制御がどうにもうまく行かなかったからRPIOで実装した。操作の系統が複数ある関係で今のサーボの状態を管理する必要が出てきたから、空いてるGPIOを出力ピンに設定して、そのピンのHigh or Lowで状態管理をしている。
音声出力については「やっぱりなんかレスポンスほしいなぁ」っていう思いから追加してみた。音声素材は「あみたろの声素材工房」様の声素材を使わせてもらった。
ゲーム等に使えるフリー声素材配布中 - あみたろの声素材工房

プログラム詳細

前回までの説明で全く出てこなかった部分を一つ一つ説明していきましょう。

初期設定

import socket
import xml.etree.ElementTree as ET
import wiringpi2 as wp
import RPi.GPIO as GPIO
import time
import sys
import threading
import pyping
import random
import pygame.mixer
from datetime import datetime
from wakeonlan import wol

# 各種状態保持用変数
room_state = 1
corridor_state = 0
alive = 0
atHome = 1
allowed = 0
counter = 0

# GPIO pin setup
servo_pin_room = 13
servo_pin_corridor = 12
state_pin_room = 2
state_pin_corridor = 3

# RPIO setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(20, GPIO.IN, GPIO.PUD_DOWN)
GPIO.setup(21, GPIO.IN, GPIO.PUD_DOWN)

# Wiring Pi setup
wp.wiringPiSetupGpio()
wp.pinMode(servo_pin_room, 2)
wp.pinMode(servo_pin_corridor, 2)
wp.pinMode(state_pin_room, 1)
wp.pinMode(state_pin_corridor, 1)
wp.pwmSetMode(0)
wp.pwmSetRange(1024)
wp.pwmSetClock(375)

room_on = int((4.75*35/90 + 7.25)*(1024/100))
corridor_off = int((4.75*5/90 + 7.25)*(1024/100))

wp.pwmWrite(servo_pin_room, room_on)
wp.pwmWrite(servo_pin_corridor, corridor_off)

wp.digitalWrite(state_pin_room, room_state)
wp.digitalWrite(state_pin_corridor, corridor_state)

# 返答音声
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=1024)
haihai = pygame.mixer.Sound("/home/pi/python_code/haihai_02.wav")
haai = pygame.mixer.Sound("/home/pi/python_code/haai_02.wav")
makasete = pygame.mixer.Sound("/home/pi/python_code/makasete_01.wav")
ok = pygame.mixer.Sound("/home/pi/python_code/ok_01.wav")
ryoukai = pygame.mixer.Sound("/home/pi/python_code/ryoukaidesu_02.wav")
itterasshai = pygame.mixer.Sound("/home/pi/python_code/itterasshai_02.wav")
misu = pygame.mixer.Sound("/home/pi/python_code/misumisu_01.wav")
oha = pygame.mixer.Sound("/home/pi/python_code/ohayou_02.wav")
okaeri = pygame.mixer.Sound("/home/pi/python_code/okaeri_03.wav")
oyasumi = pygame.mixer.Sound("/home/pi/python_code/oyasuminasai_04.wav")

初期設定部ではWiring Piの設定の他にRPIOと音声出力の設定も行っていて、RPIO設定部では2つのピン(物理スイッチにつながっているピン)を入力ピンとして設定して、音声出力設定ではpygameを呼び出して予め音声ファイルの読み込みを行っている。また、状態管理用のGPIOピンをwp.digitalWrite()で出力ピンに設定している。

各種追加関数

# 物理スイッチ用割り込み関数①
def intFuncRoom(channel):
    global room_state
    moveServo(10*room_state - 35*(room_state-1), 'room')
    room_state = not(room_state)
    wp.digitalWrite(state_pin_room, room_state)

# 物理スイッチ用割り込み関数②
def intFuncCorridor(channel):
    global corridor_state
    moveServo(5*corridor_state - 30*(corridor_state-1), 'corridor')
    corridor_state = not(corridor_state)
    wp.digitalWrite(state_pin_corridor, corridor_state)

# 在室確認用関数
def keepAlive():
    global alive
    global atHome
    global counter

    host_1 = "192.168.0.2"
    host_2 = "192.168.0.5"

    RPing_1 = pyping.ping(host_1)
    RPing_2 = pyping.ping(host_2)

    if (RPing_1.ret_code == 0) | (RPing_2.ret_code == 0):
        alive = 1
        if not(atHome) and counter > 10:
            okaeri.play()
            moveServo(35, 'room')
            moveServo(30, 'corridor')
            room_state = 1
            corridor_state = 1
            atHome = 1
        counter = 0
    else:
        alive = 0
        if counter < 100:
            counter += 1

    t = threading.Timer(1, keepAlive)
    t.start()

# サーボ状態管理用関数
def stateManage():
    global room_state
    global corridor_state

    room_state = wp.digitalRead(state_pin_room)
    corridor_state = wp.digitalRead(state_pin_corridor)

    t = threading.Timer(1, stateManage)
    t.start()

# 命令受付管理タイマ
def allowedTimer():
    global allowed

    if allowed:
        allowed = not(allowed)
        misu.play()

    print('Timer expired. allowed = ' + str(allowed))

# 返答音声再生用関数
def response(scene):
    if scene == 'hi':
        n = random.randint(1, 2)
        print n
        if n == 1:
            haai.play()
        elif n == 2:
            haihai.play()
    elif scene == 'ok':
        n = random.randint(1, 3)
        print n
        if n == 1:
            makasete.play()
        elif n == 2:
            ok.play()
        elif n == 3:
            ryoukai.play()
    elif scene == 'home':
        okaeri.play()
    elif scene == 'out':
        itterasshai.play()
    elif scene == 'morning':
        oha.play()
    elif scene == 'night':
        oyasumi.play()

intFuncRoom()とintFuncCorridor()は物理ボタンが押されたときにRPIOによって呼び出される割り込み関数で、それぞれ呼び出されたら状態を反転するようになっていて、これでスイッチ動作を実現している。

keepAlive()は「家主が家にいるかいないか」を、スマホとPCにPingを送ることで判別している。もしPingが返ってきたら alive=1 として、命令を受け付けるようになっている。
また、一定時間外出していて家に帰ってきた場合、Pingがとどいたことを条件にして家の電気をつけるような設定も入れていて、帰宅と同時に電気がついて「おかえりなさい」と声をかけてくれる。これがなかなか嬉しい。この関数はスレッドとして呼び出して、バックグラウンドで独立に動くようにしている。
別にkeep aliveしてるわけじゃないから本当は関数名を変えたほうが良いんだけど、まあいいや。

stateManage()はサーボ状態管理を一定周期で行う関数で、状態不一致を防ぐために念のため入れているだけ。こいつもスレッド呼び出し。

allowedTimer()は命令受付タイマになっていて、命令受付可能状態になったらこいつを呼び出して、一定時間だけ命令を受け付けられる状態にしている。また、命令受付可能フラグを誤検出したときにはmisu.play()が実行されて、ちょっと恥ずかしそうに「ミスミス」といってフラグをオフにするようにしている。かわいい。

response()は命令受付時に音声を再生するための関数で、初期設定で設定した音声ファイルを命令に応じて再生するようになっている。

メイン関数にはこれら状態の変更やフラグ管理の命令が追加されているけど、基本的には前回までに説明したものでできている。強いて言うならwol.send_magic_packet('xx.xx.xx.xx.xx.xx')を新たに追加していて、「パソコンつけて」の命令でWOLが動作するようになっている。これが意外と便利。引数はWOLを命令したいデバイスMACアドレス

タッチパネル制御関数は特別なことは特にやっていなくて、これまで使っていたサーボモータ制御関数をWebIOPiから呼び出せる形にしているだけ。呼び出したい関数の前に@webiopi.macroを記述するとそいつがマクロとして呼び出せるようになる。呼び出し用のHTMLファイルは以下のようにしてみた。

<!DOCTYPE html>
<html>
<head>
    <meta charset=UTF-8">
    <script type="text/javascript" src="/webiopi.js"></script>
    <script>
    webiopi().ready( function() {
        var content, button;
        content = $("#content");
        button = webiopi().createMacroButton("macro", "Room", "roomButton");
        content.append(button);

        button = webiopi().createMacroButton("macro", "Corridor", "corridorButton");
        content.append(button);
    });
    </script>
    <style type="text/css">
        button {
            display: block;
            margin: 5px 5px 5px 5px;
            width: 160px;
            height: 45px;
            font-size: 24pt;
        }
    </style>
</head>
<body>
    <div id="content" align="center"></div>
</body>
</html>

JavaScriptでボタンを生成して、コールバック関数としてpythonコードを呼び出している。webiopi().createMacroButton("macro", "Room", "roomButton")はボタン生成の関数で、第一引数がマクロ呼出しの明示、第二引数がボタン内に表示する文字列、第三引数がコールバック関数名を表している。動作の様子はこんな感じ。

ただいまシステムの全体概要はこんな感じですね。各機能の細かい動作の方法とかプログラムの記述の仕方とかは各自でググって調べてみてください。
みんなも音声操作で遊んでみてね!