ブログ

ブログです

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

長々と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")はボタン生成の関数で、第一引数がマクロ呼出しの明示、第二引数がボタン内に表示する文字列、第三引数がコールバック関数名を表している。動作の様子はこんな感じ。

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

ただいまシステムの中身③ GPIOを使ったサーボモータ制御

前回までで命令系統が完成したから、いよいよサーボモータ制御についてご紹介。今回使うサーボモータはPWM (Pulse Width Modulation, パルス幅変調)制御のやつ。
f:id:hira-hide:20170429171428p:plain
そもそもPWMってなんだよって人のために軽く説明すると、上の図みたいに一定周期(T_cycle)である幅(T_duty)だけ電圧を高くするようなやつのこと。このT_dutyを短くしたり長くしたりすると、それに応じてサーボモータが動く仕組み。

今回の流れは、まずGPIOコントロール用ソフトのWiring Piってやつをインストールして、その後にサーボ制御用プログラムを書く感じ。

今回使う「GWSサーボ PIC/STD/F」ってやつはいっくら探してもデータシートが見当たらなかったんだけど、ダメ元でSG-90と同じ値使ったらうまく動いたから助かった。

T_cycle T_duty V_high
20ms 0.5 - 2.4ms 4.8 - 5.0V

T_duty=0.5msが-90度、2.4msが90度に対応する感じ。

参考ページ

今回の参考ページはこちら。
hombre-nuevo.com
hombre-nuevo.com
http://blog.enjel.chips.jp/?eid=303blog.enjel.chips.jp

Wiring Piのセットアップ

まずはI2Cライブラリのインストールをして、その後GitでWiringPiのソースをダウンロードしてインストール、最後にWiringPi2をインストールする流れ。
なんかよく分からんけど、Raspberry Pi3でWiringPiをpythonから呼び出すときはWiringPi2のほうが良いらしい。

I2Cライブラリインストール

Raspberry Pi3のGPIOピンのうち3番、5番、27番、28番はI2Cに使えるピンで、それのセットアップのために必要なライブラリのインストール。

$ sudo apt-get install libi2c-dev

WiringPiのソースダウンロードとインストール

Gitが入ってなかったらまずはGitのインストールから。

$ sudo apt-get install git-core

そしたらGit cloneでソースコードを持ってくる。

$ git clone git://git.drogon.net/wiringPi

wiringPiっていう名前のディレクトリでコピーされるから、そこに移動してビルド。

$ cd wiringPi
$ ./build

これでWiringPi自体はインストール完了なので、WiringPi2のセットアップ。

$ sudo apt-get update
$ sudo apt-get install python3-dev python3-pip
$ sudo pip3 install wiringpi2
$ git clone https://github.com/Gadgetoid/WiringPi2-Python.git
$ cd WiringPi2-Python
$ sudo python3 setup.py install

これでOK。

サーボモータを動かす

Raspberry Piサーボモータを動かすためには最初に書いたようにPWMを出力しなきゃいけないんだけど、Raspberry Pi3でPWM出力にハードウェアで対応してるGPIOピンは下図のPWMxと書かれてるピンのみ。
f:id:hira-hide:20170507123830p:plain
それ以外のピンでPWMをやろうとするとソフトウェア的にやらなきゃいけなくてめんどうだから、PWM対応ピンを使うのが無難(一応WiringPiの機能でSoftware PWMもあるらしい)。

さて、WiringPiでサーボモータを動かす時には pwmWrite っていう関数を使う。

void pwmWrite(int pin, int value);
Writes the value to the PWM register for the given pin. The Raspberry Pi has one on-board PWM pin, pin 1 (BMC_GPIO 18, Phys 12) and the range is 0-1024. Other PWM devices may have other PWM ranges.

引数にピン番号(int pin)と動かしたい角度(int value)を入力するんだけど、動かしたい角度は直接角度を入力するんじゃなくて「T_cycleのうちの何割をT_dutyに充てるか」を用いて指定する。
例えば「90度動かしたいなぁ」ってときは、上で書いた表から計算すると 2.4ms/20ms=12% と計算できる。
さらに、上の関数説明文の中に and the range is 0-1024 って書いてあるように、デフォルトだと 0=0%, 1024=100% を意味するようになってるから、最終的に与える引数は int(12*1024/100) となる。

サーボ制御プログラム

さーてここまで来たらあとはプログラムを書いて動かすだけだ!

#!/usr/bin/python

import wiringpi2 as wp
import sys

servo_pin = 12    # 12番ピンを指定
param = sys.argv
set_degree = int(param[1])
print(set_degree)

wp.wiringPiSetupGpio()    # 上図 pin(BOARD) の番号でピン指定するモード
wp.pinMode(servo_pin, 2)  # 出力ピンとして指定
wp.pwmSetMode(0)          # 0Vに指定
wp.pwmSetRange(1024)      # レンジを0~1024に指定
wp.pwmSetClock(375)       # 後述

move_deg = int((4.75*set_degree/90 + 7.25)*(1024/100)) # 後述
wp.pwmWrite(servo_pin, move_deg)

実行時の引数に角度を指定してこのプログラムを実行すると、指定した角度だけちゃんとサーボが動いてくれる。

さて、上で後述とした二点について。wp.pwmSetClock() はWiring PiのPWM制御のための基準周波数を決める関数で、以下の条件式が成立するらしい。

PWM周波数=Raspberry PiのPWMが持つベースクロックの周波数/(clock×レンジ)

いま、サーボのPWM周波数が50Hz、Raspberry PiのPWMが持つベースクロックの周波数が19.2MHz、レンジが1024だから、clock=18750/50=375 になる。
move_deg = int( (4.75*set_degree/90 + 7.25)*(1024/100) ) ではサーボ動作の引数を計算してるんだけど、ここでは「0度を中心として何度動かすか?」を計算している。
0度のときが7.25%、90度のときが12%、-90度のときが2.5%だから、その間を線形に計算するような式を突っ込んでるだけ。

これで音声認識、サーボ動作、2つのコアとなる機能ができたから、あとはこいつらをまとめ上げればただいまシステムは完成だ!

ただいまシステムの中身② Socketを介した認識結果のXMLパース

前回までで音声認識をする部分は完成したから、今回は認識結果を出力して渡す部分について。
まずはJuliusをモジュールモードで起動して、その後にpythonでsocketつないでxmlをもらう、といった流れ。
f:id:hira-hide:20170423165131p:plain
前回の最後にサーボ制御についても説明するとか書いてたけど、それはさらに次回。

参考ページ

今回のはほぼこちらのページを参考にさせてもらった。
blog.livedoor.jp

Juliusのモジュールモード起動

Julius起動時のオプションに -module を付けるとモジュールモードでの起動になって、クライアントからの TCP/IP 接続待ち状態になる。その状態でクライアントからの接続を受けると音声認識可能な状態になって、クライアントに認識結果をXMLで送信するようになる。
より詳細については公式ドキュメントを参照してね。
第10章 モジュールモード

ちなみに、いちいちオプションに -module つけんのめんどくせぇよってときは、前回作った設定ファイル(word.jconf)に -module を書いとけば勝手にモジュールモードにしてくれる。

Julius出力XML

JuliusのXMLは以下のような構成になってるらしい。公式ページから引用。改行コードは "\n"で、クライアントがパーズしやすくするために、メッセージ送信ごとにデータの終端として"." のみの行が送信される。

<STARTPROC/>
<INPUT STATUS="LISTEN" TIME="994675053"/>
<INPUT STATUS="STARTREC" TIME="994675055"/>
<STARTRECOG/>
<INPUT STATUS="ENDREC" TIME="994675059"/>
<GMM RESULT="adult" CMSCORE="1.000000"/>
<ENDRECOG/>
<INPUTPARAM FRAMES="382" MSEC="3820"/>
<RECOGOUT>
  <SHYPO RANK="1" SCORE="-6888.637695" GRAM="0">
    <WHYPO WORD="silB" CLASSID="39" PHONE="silB" CM="1.000"/>
    <WHYPO WORD="上着" CLASSID="0" PHONE="u w a g i" CM="1.000"/>
    <WHYPO WORD="を" CLASSID="35" PHONE="o" CM="1.000"/>
    <WHYPO WORD="白" CLASSID="2" PHONE="sh i r o" CM="0.988"/>
    <WHYPO WORD="に" CLASSID="37" PHONE="n i" CM="1.000"/>
    <WHYPO WORD="して" CLASSID="27" PHONE="sh i t e" CM="1.000"/>
    <WHYPO WORD="下さい" CLASSID="28" PHONE="k u d a s a i" CM="1.000"/>
    <WHYPO WORD="silE" CLASSID="40" PHONE="silE" CM="1.000"/>
  </SHYPO>
</RECOGOUT>
.

これをXMLパーサで受け取って解析すれば、認識結果の信頼度命令を抽出できるから、音声制御が可能になる。

Python処理

今回プログラムはすべてPythonで書いた。理由は特になくて、みんなRaspberry Pi動かすときはPython使ってるっぽいからそうしただけ。サンプルコード多いと助かるしね。
まずはとりあえずプログラム全体の掲載。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
import xml.etree.ElementTree as ET

def main():
    host = 'localhost'
    port = 10500

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

    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'))
                    
                    if command == u'ただいま' and score >= 0.9:
                        # ここにただいま処理
                    elif command == u'パキン' and score >= 0.996:
                        # ここにパキン処理
                    elif command == u'おやすみ' and score >= 0.93:
                        # ここにおやすみ処理
                    elif command == u'いってきます' and score >= 0.93:
                        # ここにいってきます処理
                    elif command == u'部屋つけて' and score >= 0.93:
                        # ここに部屋つけて処理
                    elif command == u'おはよう' and score >= 0.9:
                        # ここにおはよう処理
                    elif command == u'部屋消して' and score >= 0.9:
                        # ここに部屋消して処理
                    elif command == u'廊下つけて' and score >= 0.93:
                        # ここに廊下つけて処理
                    elif command == u'廊下消して' and score >= 0.9:
                        # ここに廊下消して処理
                data = ''
            else:
                data = data + client.recv(1024)
    except KeyboardInterrupt:
        client.close()

if __name__ == "__main__":
    main()

個々の処理は次回以降また説明するとして、今回はsocketとXML解析についてだけ。

Socket接続部分

host = 'localhost'
port = 10500

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

Socketについてはこれだけ。あ、もちろん最初に import socket は忘れないように。
今回は Host も Client も自分自身だから、IPアドレスlocalhost として、ポートはデフォルトの10500番
この宣言で socket が接続されるから、音声認識待ち状態になって認識結果をXMLで投げるようになる。

XML解析部分

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'))
                
                if command == u'ただいま' and score >= 0.9:
                    # ここにただいま処理
                elif command == u'パキン' and score >= 0.996:
                    # ここにパキン処理
                elif command == u'おやすみ' and score >= 0.93:
                    # ここにおやすみ処理
                elif command == u'いってきます' and score >= 0.93:
                    # ここにいってきます処理
                elif command == u'部屋つけて' and score >= 0.93:
                    # ここに部屋つけて処理
                elif command == u'おはよう' and score >= 0.9:
                    # ここにおはよう処理
                elif command == u'部屋消して' and score >= 0.9:
                    # ここに部屋消して処理
                elif command == u'廊下つけて' and score >= 0.93:
                    # ここに廊下つけて処理
                elif command == u'廊下消して' and score >= 0.9:
                    # ここに廊下消して処理
            data = ''
        else:
            data = data + client.recv(1024)
except KeyboardInterrupt:
    client.close()

まず data という空の変数を作って、whileでずっとループを回して受け取ったデータをこの変数に格納できるようにする。
上に書いたように Julius の XML は \n. で終端されるから、それを判定基準として if を入れている。
PythonXML パーサでちゃんと処理できるように、まず xml ヘッダと改行コード(\n)を付け足して、RECOGOUT 要素以下を XML としてパースする。

実際の認識結果は タグとして渡されるから、whypo に格納して、value を見て判定を行っていく。ここでは WORD(認識した単語)と CM(信頼度)を抽出して命令実行のための判定に使っている。
f:id:hira-hide:20170423181632j:plain
print命令で WORD と CM を出力した結果はこんな感じ。あとは処理部分を記述すればだいたいおわり。

次回はサーボモータ制御について書く予定。

ただいまシステムの中身① Raspberry Piで音声認識

まず前回のおさらい。↓みたいなシステムを作った。
f:id:hira-hide:20170416211614p:plain
外観写真で説明すると、USBマイクで音声命令を拾って、それをRaspberry Piで認識して、しかるべき制御をサーボモータに送るっていう流れになる。
f:id:hira-hide:20170422162903j:plain
今回は最初の音声認識部分について書いていくゾ。手順としては、まず音声入力デバイスの優先度を変更して、Juliusっていう音声認識ソフトのセットアップをするだけ。

参考ページ

音声認識を行う上で以下のWebページを参考にさせてもらった。
qiita.com
blog.livedoor.jp
blog.livedoor.jp

USBマイクのセットアップ

事前準備

USBマイクを使えるようにするために、まずはパッケージのアップデートと各種ドライバ・ソフトウェアのインストール、マイクの動作設定とかをやってくよ。

とりあえずパッケージのアップデートから

パッケージアップデート
$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo rpi-update

アップデートが終わったらライブラリのインストール

ライブラリインストール
$ sudo apt-get install alsa-utils sox libsox-fmt-all

ちなみに、ALSA (Advanced Linux Sound Architecture)はLinux用音声ドライバ、soxは音声ファイル形式変換ソフトらしい(上記リンク先のコマンドそのまま打っただけ)

さて、ここまでやったらとりあえずUSBポートにマイクを差して、まずはちゃんとハードウェアが認識されているかのチェック。

ハードウェア認識チェック
$ lsusb
Bus 001 Device 004: ID 0d8c:0016 C-Media Electronics, Inc.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

C-Media Electronics, Inc.がUSBマイクらしいから、これが表示されてれば認識はOK

オーディオモジュールの優先順位変更

おそらく初期設定だと内蔵オーディオモジュールの優先順位のほうが高いはずなので、/proc/asound/modules を見て確認。

モジュール優先度チェック
$ cat /proc/asound/modules
 0 snd_bcm2835
 1 snd_usb_audio

↑みたいに snd_bcm2835(内蔵オーディオモジュール)が0番だったら、/etc/modprobe.d/alsa-base.conf を変更してUSBマイクの優先順位を変えよう。
この設定ファイルが生成されていないこともあるから、そのときは新規作成

alsa-base.conf の変更(or新規作成)
options snd slots=snd_usb_audio,snd_bcm2835
options snd_usb_audio index=0
options snd_bcm2835 index=1

これでRebootすれば優先順位が入れ替わってるはず。

マイク感度設定

音声入力するにあたって、マイク感度を適当に調節しとかないといけないから、それの設定。と、その前にサウンドカードの番号をチェック!

サウンドカード番号チェック
$ arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: Micophone [USB Micophone], device 0: USB Audio [USB Audio]
  Subdevices: 0/1
  Subdevice #0: subdevice #0

card 0 と書いてあればサウンドカード番号は0番だから、感度設定の引数で0を設定

マイク感度設定
$ amixer sset Mic 62 -c 0
Simple mixer control 'Mic',0
  Capabilities: cvolume cvolume-joined cswitch cswitch-joined penum
  Capture channels: Mono
  Limits: Capture 0 - 62
  Mono: Capture 62 [100%] [22.50dB] [on]

マイク感度は0 - 62の63段階で選ぶようになってるけど、まーたぶん最大値で設定して問題ないと思う(ぼくは今のところ問題ない)。

これでUSBマイクは使える状態になったから、こっから音声認識ソフトの設定をやってくよ。

Juliusのインストールと設定

そもそもこのJuliusっていうソフトは何なのかっつーことだけど、公式サイトからまんま引用した以下の文章を読んでくれ。

Julius は,音声認識システムの開発・研究のためのオープンソースの高性能な汎用大語彙連続音声認識エンジンです. 数万語彙の連続音声認識を一般のPCやスマートフォン上でほぼ実時間で実行できる軽量さとコンパクトさを持っています.

言語モデルとして単語N-gram,記述文法,ならびに単語辞書を用いることができます.また音響モデルとしてトライフォンのGMM-HMMおよびDNN-HMMを用いたリアルタイム認識を行うことができます.DNN-HMMの出力計算にnumpyを用いた外部モジュールを利用することも可能です.複数のモデルや複数の文法を並列で用いた同時認識も行うことができます.

Juliusの最大の特徴はその可搬性にあります.単語辞書や言語モデル・音響モデルなどの音声認識の各モジュールを組み替えることで,小語彙の音声対話システムからディクテーションまで様々な幅広い用途に応用できます.

Julius はオープンソースソフトウェアです.プログラムはC言語で書かれており、さまざまなプラットフォームへの移植や改造が容易です.ライセンスはオープンライセンスで,商用利用への制限もありません.

http://julius.osdn.jp/

細かい話は専門家じゃないからよく分からんけど、動きは下の画像のような感じ。
f:id:hira-hide:20170422204747p:plain
音声入力を文字データにして、あらかじめ用意してある辞書データと比較して合致するものを探すみたい。
wgetJulius本体ディクテーションキット文法認識キットの3つをインストール。バージョンは適宜変えてくれ。あとディクテーションキットは容量がデカイから時間かかるかも。

Julius本体、ディクテーションキット、文法認識キットのダウンロードとインストール
sudo wget -O julius-4.4.2.tar.gz 'http://sourceforge.jp/frs/redir.php?m=osdn&f=%2Fjulius%2F60273%2Fjulius-4.3.1.tar.gz'
sudo wget -O dictation-kit-v4.3.1-linux.tgz 'http://sourceforge.jp/frs/redir.php?m=jaist&f=%2Fjulius%2F60416%2Fdictation-kit-v4.3.1-linux.tgz'
sudo wget -O grammar-kit-v4.1.tar.gz 'http://sourceforge.jp/frs/redir.php?m=osdn&f=%2Fjulius%2F51159%2Fgrammar-kit-v4.1.tar.gz'

tar zxvf julius-4.4.2.tar.gz
cd julius-4.4.2/
./configure
make
sudo make install

cd ..
tar zxvf dictation-kit-v4.3.1-linux.tgz
tar zxvf grammar-kit-v4.1.tar.gz

これでとりあえず使えるようにはなってるから、テストしてみよう。

話しかけてみよう!

あらかじめ用意されてるグラマーキット testmic.jconf を使ってきちんと認識するかどうかのテスト。julius -C ./grammar-kit-v4.1/testmic.jconf -charconv EUC-JP UTF-8 を実行すると音声認識スタンバイ状態になるから、話しかけると「あんたが今言ったのこれやな!」って返してくれるよ。
ちなみに、このテスト設定だと「みかん・りんご・ぶどう」の3つの果物について反応するから、適当に「みかん!」とか「りんご!」とか言ってみてね。

Julius動作テスト
julius -C ./grammar-kit-v4.1/testmic.jconf -charconv EUC-JP UTF-8

Notice for feature extraction (01),
        *************************************************************
        * Cepstral mean normalization for real-time decoding:       *
        * NOTICE: The first input may not be recognized, since      *
        *         no initial mean is available on startup.          *
        *************************************************************
 
Stat: adin_oss: device name = /dev/dsp (application default)
Stat: adin_oss: sampling rate = 16000Hz
Stat: adin_oss: going to set latency to 50 msec
Stat: adin_oss: audio I/O Latency = 32 msec (fragment size = 512 samples)
STAT: AD-in thread created

この状態の時に話しかければOK。ちゃんと結果が返ってきたら設定とかは問題なし。

辞書データ作成

ここまで来たらあとは自分の用途に合わせて辞書データを作成してあげればOK。認識結果はsocketを介してxmlで渡せるから、それを使って後段のサーボモータ制御をやっていく流れ。
辞書ファイルは
(単語)(タブ区切り)(読み)
の形で記述して、拡張子は.yomiとして保存すること。今回の目的は「ただいま」に反応して動作してくれればいいから基本的には「ただいま」だけ辞書登録しとけば良いんだけど、どうせならいろいろやりたいな、と思って以下のような辞書データを作った。

ただいま    ただいま
おやすみ    おやすみ
部屋つけて   へやつけて
部屋消して   へやけして
廊下つけて   ろうかつけて
廊下消して   ろうかけして
おはよう    おはよう
いってきます  いってきます
パキン     ぱきん

機能としては、

ただいま   → 部屋も廊下もつく
おやすみ   → 部屋も廊下も消える
部屋つけて  → 部屋の電気がつく
部屋消して  → 部屋の電気が消える
廊下つけて  → 廊下の電気がつく
廊下消して  → 廊下の電気が消える
おはよう   → 部屋の電気がつく
いってきます → 部屋も廊下も消える
パキン    → 部屋も廊下もつく

ってなってる。パキンってなんだよパキンってと思うかもしれないけど、これは指パッチンで部屋の電気ついたらクソおもしれぇなと思ってあとから追加した辞書データ。ちなみに動きはこんな感じ。


超おもしろかった。その代わり判定しきい値をかなり高くしてるからめったに動作しない。隠しコマンド的立ち位置。

それはさておき、辞書データを作ったらJulius用の辞書形式ファイルに変換する。
あ、ルートディレクトリは基本的に home を考えて書いてるから、適宜読み替えてください。

辞書データ変換
cd julius-4.4.2/gramtools/yomi2voca
iconv -f utf8 -t eucjp ~/word.yomi | yomi2voca.pl > ~/dictation-kit-v4.3.1-linux/word.dic

そしたら最後に設定データを作成すれば音声認識基盤の完成!

$ vi ./dictation-kit-v4.3.1-linux/word.jconf

-w word.dic       #単語辞書ファイル
-v model/lang_m/bccwj.60k.htkdic  #N-gram、または文法用の単語辞書ファイルを指定
-h model/phone_m/jnas-tri-3k16-gid.binhmm #使用するHMM定義ファイル
-hlist model/phone_m/logicalTri   #HMMlistファイルを指定する
-n 5        #n個の文仮説数が見つかるまで検索を行う
-output 1     #見つかったN-best候補のうち、結果として出力する個数
-input mic      #マイク使用
-input oss      #オープンサウンドシステム使用
-rejectshort 600  #検出された入力が閾値以下なら棄却
-charconv euc-jp utf8 #入出力エンコード指定(内部euc-jp, 出力utf-8)
-lv 1000    #入力の振幅レベルの閾値(0~32767)

これで julius -C ./dictation-kit-v4.3.1-linux/word.jconf を実行すれば上で作った辞書データを元に音声認識してくれる。

認識結果の受け渡しとそれを利用したサーボ制御については次回。

ただいまシステム

ただいまシステムと言うものをつくったお話です。

ことの発端

これまでずっと実家にいたけど、ついに家を出て一人暮らしをすることになった。一人暮らしすることはまあ別にいいんだけど、しばらく暮らしていてどうも何か物足りないな、ということに気づいてきた。

【これまで】
ぼく「ただいまー」
家族「おかえりー」
【いま】
ぼく「はぁ~~……よいしょっと(部屋の電気をつける)」

そう、ただいまがないのだ!

ほしいなぁ~~~ただいま・おかえりプロトコル

せや!ただいまに反応するなんか作ったろ!

ということで秋葉原に走った。

とりあえずこのラズベリーパイっていうの買えばいいんだろ?

たぶんラズベリーパイ買えばなんとかなるだろ!と思って秋葉原でいろいろ買った。

f:id:hira-hide:20170416205111j:plain

  • Raspberry Pi 3 model B
    WiFiは標準でついててほしかったからこれ
  • PiShow 2.8
    SPIで表示させるタッチパネル
  • GWSサーボ PIC/STD/F
    ちっちゃいサーボモーター。2個。
  • USBマイク MM-MCU02BK
    写真には載ってないけどサンワサプライのUSBマイク
  • あとこまごまとしたもの

やりたいことは↓みたいなこと。

f:id:hira-hide:20170416211614p:plain

本当はPhilips Hueとか使ってもっと賢くやりたかったんだけど、家の電気が全部蛍光灯だったから仕方なく電気のスイッチをサーボモータで物理的に動かすことにした。

出来上がったシステム

外観はこんな感じ。本当はラズベリーパイのGPIOに直接サーボつなぐのはよろしくないけど、ちっちゃいサーボだし大丈夫でしょ!

f:id:hira-hide:20170416212147j:plain

ただいまっていうとちゃんと電気つくよ!

次回以降セットアップとかプログラムとか紹介する予定。

ちょっとした一工夫で生活は遥かに豊かになる

社会人になってしまってから生活の大部分が会社で過ごす時間になって、日々の面白エピソードが必然的に社内話になっちゃってブログにはかけずヤキモキしていたけれど、「フィギュアレビュー書いてよ」って言われたから今日はそんなようなことを書くね。

ただ、フィギュアレビューに関してはぼくなんかよりずっと巧みな言葉遣いお金の掛かった撮影機材とんでもなく歓迎意欲の湧く記事を書いてくださる方々がいらっしゃるので、普通のレビューはそういう方々におまかせしてここでは撮影が楽しくなる一工夫を紹介。

被写体にはこの子を使います。ジャジャーン!

f:id:hira-hide:20161120101505j:plain

ToHeart2 AnotherDays  菜々子 ~ネコ耳スクール水着ver.~黒ネコ版」です。なんど見てもハチャメチャにかわいい。お迎えしてよかった。まだamazonでも取り扱いはあるみたいだけど定価の倍くらいの値段になってる。

 さてさてこの菜々子フィギュア、原型がとても良いからスマートフォンで適当にパシャリとしても常に性的に切り出されるとってもすばらしいフィギュアです。

f:id:hira-hide:20161120102222j:plainこんなのとか。

f:id:hira-hide:20161120102248j:plainもうちょっと胸側に寄ったのとか。

でもでも、ちょ~~っとだけ工夫を加えるだけでこの写真がさらにアトラクティングエキサイティングエレクチオンになっちゃうんだな。

手順①:適当な紙と穴あけパンチを用意!

f:id:hira-hide:20161120102559j:plain

手順②:適当に一つ穴を開ける!

f:id:hira-hide:20161120102721j:plain

手順③:穴越しに撮影!

f:id:hira-hide:20161120102806j:plain

すごい!すごい背徳感をかんじる!

f:id:hira-hide:20161120102933j:plain

みんなもやってみてね!

四の五の言わずプリペイドSIMを買え

学生の頃に(晴れて修了したのでやっとこの言葉が使える)何度も海外出張したけど、行く国行く国で現地のプリペイドSIMを買うのがぼくの楽しみの一つでもあったわけだけど、今回はミュンヘンに行った時に大変困った事になったのでそのことでも書きますね。

続きを読む