Breakout(2) ロータリーエンコーダとマウス

PyGameで作ってみたブロック崩し、ロータリーエンコーダのプログラムと組み合わせて、パドルコントローラ風に遊べるバージョンも作ってみた。

動くには動いたのだが、どうもあまり動きが滑らかではない。やはりPythonだけでロータリーエンコーダの検出をやらせるのは無理があるのだろうか?GPIOまわりのアクセスをC言語で直接叩くようにすればもっと高速に取り込むことができて、滑らかに動かせると思うのだが、そこまで突っ込んでやってしまうと遊びの範疇からずれてしまう気がするので、今のところはここまでにしておく。

その代わりに、マウス入力で遊べるバージョンも作成。こっちはレスポンスも良く、ゲームとしてかなり遊べる状態になった。

ソースコードは長くなるので、ロータリーエンコーダ版とマウス版の両方をまとめてGoogle Driveに置いた。ここからダウンロードできる。Breakout_m.pyがマウス版。

マウスを入力デバイスとした場合、デフォルトではマウスポインタがウィンドウを外れるとパドルが動かなくなってしまうため、それではゲームにならない。PyGameはマウスポインタ(マウスカーソル)が非表示で、なおかつ pygame.event.set_grab がTrueに設定されている時は、常にマウスイベントが送られてくるようになっている。

その代わり他のプログラムにマウスやキーボードイベントが送られなくなってしまうので、注意が必要だ。プログラムを終了するためにウィンドウの[閉じる]をクリックする事もできなくなるので、QUITイベントで終了する代わりにエスケープキーによって終了するように変更した。

    pygame.mouse.set_visible(False)
    pygame.event.set_grab(True)
    mx, my = pygame.mouse.get_rel()
    while True:
        clock.tick(60)
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN and event.key == K_ESCAPE:
                pygame.mixer.quit()
                pygame.quit()
                sys.exit()
        mx, my = pygame.mouse.get_rel()
        paddle.rect.left = paddle.rect.left + mx

サンプルのつもりで作ってみたゲームだが、マウス版は意外と遊べたので、もう少し手を加えればちゃんとしたゲームになりそう。時間があればやってみるかも知れないが、どうせならオリジナリティのあるゲームも作ってみたい。良いアイデアが思い付けばの話だけど。

Breakout

Raspberry Piにロータリーエンコーダを接続して値を読み取る事はできたので、これを何かに応用したいと考えた。真っ先に思い付いたのは「ブロック崩し」のパドルコントローラとしての応用で、そもそも秋月でロータリーエンコーダを購入したのは、Arduinoのゲームコントローラに使えないか?と考えての事。

結局Arduinoで使うことは無かったが、Raspberry Piならゲーム作成用のライブラリも存在しているので、試しに作ってみる事にした。実を言うと前回の「PyGameでサウンド再生」はその過程で引っかかった事なのだ。

まずはロータリーエンコーダを使わずに、キーボードで操作できるバージョンを作ってみた。プログラムは以下の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import pygame
from pygame.locals import *

screen_rect = Rect(0, 0, 480, 640)
white = 255, 255, 255
black = 0, 0, 0
flip = ((10, 10, 10, 10,  8,  8,  8,  8,
          8,  8,  8,  8,  8,  8,  8,  8,
          8,  8,  8,  8,  8,  8,  6,  6,
          6,  6,  6,  6,  4,  4,  4,  4),
        ( 4,  4,  4,  4,  8,  8,  8,  8,
          8,  8,  8,  8,  8,  8,  8,  8,
          8,  8,  8,  8,  8,  8,  8,  8,
          8,  8,  8,  8, 10, 10, 10, 10))

class Block(pygame.sprite.Sprite):
    def __init__(self, filename, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(filename).convert()
        ckey = self.image.get_at((0,0))
        self.image.set_colorkey(ckey, RLEACCEL)
        w = self.image.get_width()
        h = self.image.get_height()
        self.rect = Rect(x * 48 + 1, y * 16 + 64, w, h)
    def update(self, ball):
        pass
    def draw(self):
        screen.blit(self.image, self.rect)

class Ball(pygame.sprite.Sprite):
    def __init__(self, filename):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(filename).convert()
        ckey = self.image.get_at((0,0))
        self.image.set_colorkey(ckey, RLEACCEL)
        w = self.image.get_width()
        h = self.image.get_height()
        self.rect = Rect(10, 200, w, h)
        self.speed = [8, 8]
    def update(self):
        self.rect.move_ip(self.speed)
        if self.rect.left <= 0 or self.rect.right > screen_rect.width:
            self.speed[0] = -self.speed[0]
        if self.rect.top <= 0:
            self.speed[1] = -self.speed[1]
        self.rect = self.rect.clamp(screen_rect)
    def draw(self):
        screen.blit(self.image, self.rect)
    def reset(self):
        self.rect = Rect(10, 200, self.rect.width, self.rect.height)
        self.speed = [8, 8]

class Paddle(pygame.sprite.Sprite):
    def __init__(self, filename):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(filename).convert()
        ckey = self.image.get_at((0,0))
        self.image.set_colorkey(ckey, RLEACCEL)
        w = self.image.get_width()
        h = self.image.get_height()
        self.rect = Rect(208, 592, w, h)
        self.speed = [0, 0]
    def update(self):
        self.rect.move_ip(self.speed)
        self.rect = self.rect.clamp(screen_rect)
    def draw(self):
        screen.blit(self.image, self.rect)

def set_block(blocks):
    for y in range(0, 8):
        for x in range(0, 10):
            if y < 2:
                block = Block("block_red.png", x, y)
            elif y < 4:
                block = Block("block_orange.png", x, y)
            elif y < 6:
                block = Block("block_green.png", x, y)
            elif y < 8:
                block = Block("block_yellow.png", x, y)
            blocks.add(block)

if __name__ == '__main__':
    pygame.mixer.pre_init(frequency = 44100, size = -16, channels = 2, buffer = 1024)
    pygame.init()
    screen = pygame.display.set_mode(screen_rect.size)
    #bg = pygame.image.load("breakout_bg.png").convert()
    pygame.event.set_allowed([QUIT, KEYDOWN, KEYUP])
    font = pygame.font.Font(None, 40)
    ball = Ball("ball.png")
    paddle = Paddle("paddle.png")
    sound01 = pygame.mixer.Sound('se01.wav')
    sound02 = pygame.mixer.Sound('se02.wav')
    blocks = pygame.sprite.RenderUpdates()
    set_block(blocks)
    clock = pygame.time.Clock()
    pmove = [0, 0]
    state = 0
    bleft = 3
    tick = pygame.time.get_ticks()
    while True:
        clock.tick(60)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.mixer.quit()
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN:
                if event.key == K_LEFT:
                    pmove[0] = -8
                elif event.key == K_RIGHT:
                    pmove[1] = 8
            elif event.type == KEYUP:
                if event.key == K_LEFT:
                    pmove[0] = 0
                elif event.key == K_RIGHT:
                    pmove[1] = 0
        paddle.speed = [pmove[0] + pmove[1], 0]
        screen.fill(black)
        #screen.blit(bg, screen_rect)
        paddle.update()
        if state == 1:
            ball.update()
            if pygame.sprite.spritecollideany(ball, blocks):    # Hit Block?
                sound01.play()
                for block in blocks:
                    if pygame.sprite.collide_rect(ball, block):
                        clip = block.rect.clip(ball.rect)
                        if(clip.height > clip.width):
                            ball.speed[0] = -ball.speed[0]
                        else:
                            ball.speed[1] = -ball.speed[1]
                        blocks.remove(block)
            if pygame.sprite.collide_rect(ball, paddle):        # Hit Paddle?
                sound02.play()
                ball.speed[1] = -ball.speed[1]
                clip = paddle.rect.clip(ball.rect)
                p = clip.left - paddle.rect.left + clip.width / 2
                if p >= 32:
                    p = 63 - p
                vx = flip[0][p]
                if ball.speed[0] < 0:
                    vx = -vx
                ball.speed[0] = vx
                ball.speed[1] = -flip[1][p]
            ball.draw()
        blocks.draw(screen)
        paddle.draw()
        if state == 0:
            text = font.render('Ready!', True, (255, 255, 255))
            screen.blit(text, (190, 300))
            if tick + 2000 < pygame.time.get_ticks():
                state = 1
        elif state == 1:
            if ball.rect.bottom >= screen_rect.height:
                ball.reset()
                bleft = bleft - 1
                if bleft > 0:
                    state = 2
                else:
                    state = 3
                tick = pygame.time.get_ticks()
        elif state == 2:
            msg = "Ball Left : " + str(bleft)
            text = font.render(msg, True, (255, 255, 255))
            screen.blit(text, (175, 300))
            if tick + 2000 < pygame.time.get_ticks():
                state = 1
        elif state == 3:
            text = font.render('GAME OVER', True, (255, 255, 255))
            screen.blit(text, (160, 300))
            if tick + 2000 < pygame.time.get_ticks():
                pygame.mixer.quit()
                pygame.quit()
                sys.exit()
        if not blocks and ball.rect.top > 320:
            set_block(blocks)   # All Blocks Clear
        pygame.display.update()

動かす為には画像ファイルとサウンドファイルも必要なので、ソースコードと必要なファイルはまとめてGoogle Driveへ置いた。ここにアクセスして、画面左上の「ファイル」をクリックしてメニューを開き、「ダウンロード」を選択。

GoogleDrive

解凍してRaspberry Piにすべてのファイルを転送したら

$ python Breakout.py

で起動する。
操作はカーソルキーの左右のみ。
Breakout_cap

PyGameでゲームを作るのは初めてなので、あちこち無駄があったり良くない使い方があるような気がするが、大目に見て欲しい。Python2.7とPyGameがインストールされていれば、Windowsでも動作するはず(MacOSは未確認)。
最初は背景画像も表示するようにしていたのだが、ボールが見えにくくなるだけだったのでコメントアウトして外してある。良さそうな画像があったら差し替えてみても面白いかも。ブロックとボール、パドルも単なる画像なので差し替え可能。

次はこれを元に、ロータリーエンコーダーでパドルを操作できるバージョンを作ってみる予定。

PyGameでサウンド再生

RaspberryPiでPythonを使ってサウンドを再生してみようと思い、調べてみるとPyGameに含まれるサウンド機能を使うのが簡単そうだったので試してみたのだが、意外と手間取ってしまった。

テスト用にフリーの音源サイトからダウンロードしたwavファイルを、Raspbianの最新版にはデフォルトで入っているaplayで再生してみると、特に問題無く再生される。この時にwavファイルのビットやレートも表示されるので、それに合わせてテストプログラムを作ってみた。

aplay

import pygame
from pygame.locals import *

pygame.mixer.init(frequency = 22050, size = 8, channels = 1, buffer = 1024)
sound = pygame.mixer.Sound("se.wav")
sound.play()
try:
    while True:
        pass
except KeyboardInterrupt:
    pygame.mixer.quit()

ところがこれでは全く再生されない。音声はHDMI経由でモニタのスピーカーから出るようにしてあって、ボリュームを最大に上げると一瞬ノイズのような音が聞こえるのだが、それっきり。

色々と試してみると、再生しようとするwavファイルとは異なる周波数に初期化すると再生される事が分かったのだが、なぜそうなるのかは不明。以下のプログラムなら正しく再生される。

import pygame
from pygame.locals import *

pygame.mixer.init(frequency = 44100, size = -16, channels = 2, buffer = 1024)
sound = pygame.mixer.Sound("se.wav")
sound.play()
try:
    while True:
        pass
except KeyboardInterrupt:
    pygame.mixer.quit()

PyGameのサウンド再生はwavファイルだけではなく、mp3ファイルをエンドレスで再生する機能もあって、wavファイルと同時に再生できるのでその機能を使ってこんなプログラムを作ってみた。デフォルトで入っているmp3を再生しながら、キーボードの1~4を押した時に対応するwavを再生するだけ。PyGameでウィンドウを作り、そこに文字を表示させるのも一緒に試してみた。

#!/usr/bin/env python

import sys, pygame
from pygame.locals import *

if __name__ == "__main__":
    pygame.init()
    pygame.mixer.quit()
    pygame.mixer.init(frequency = 48000, size = -16, channels = 2, buffer = 1024)
    pygame.mixer.music.load("/usr/share/scratch/Media/Sounds/Music Loops/Xylo1.mp3")
    p1 = pygame.mixer.Sound("/usr/share/scratch/Media/Sounds/Percussion/Gong.wav")
    p2 = pygame.mixer.Sound("/usr/share/scratch/Media/Sounds/Percussion/HandClap.wav")
    p3 = pygame.mixer.Sound("/usr/share/scratch/Media/Sounds/Percussion/CymbalCrash.wav")
    p4 = pygame.mixer.Sound("/usr/share/scratch/Media/Sounds/Percussion/DrumBuzz.wav")
    size = width, height = 200, 200
    screen = pygame.display.set_mode(size)
    font = pygame.font.Font(None, 40)
    text = font.render('Sound Test', True, (255, 255, 255))
    screen.blit(text, (20, 80))
    pygame.display.update()
    pygame.event.set_allowed([QUIT, KEYDOWN])
    pygame.mixer.music.play(-1)
    try:
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.mixer.quit()
                    pygame.quit()
                    sys.exit()
                if event.type == KEYDOWN:
                    if event.key == K_1:
                        p1.stop()
                        p1.play()
                    elif event.key == K_2:
                        p2.stop()
                        p2.play()
                    elif event.key == K_3:
                        p3.stop()
                        p3.play()
                    elif event.key == K_4:
                        p4.stop()
                        p4.play()
    except KeyboardInterrupt:
        pygame.mixer.quit()
        pygame.quit
        sys.exit()

デスクトップ画面にウィンドウを表示するので、これはリモートでは実行できない。VNCのリモートデスクトップでなら動かせるが、サウンドがHDMIモニタに出力されるのでVNCでは意味がない。
soundtstPyGameは初めて使ってみたのだが、昔のBASICパソコン時代のようなゲームなら割と簡単に作れそうだ。Pythonがあまり速くないので今風のゲームは無理だろうけど、チュートリアルサイトを見るとスーパーファミコン時代のゲームなら作成可能という説明があって、確かにそんな感じがする。

time.clock()

前回試したロータリーエンコーダのプログラム、起動してしばらくはちゃんと動くのだが、30分ぐらい放置すると動かなくなってしまう事が分かった。

ソースコードを机上で確認すると、チャタリング除去に使った time.clock() の戻り値が増加せずに減少していたとすれば、発生している症状と合致する。最初はタイマの値がオーバーフローを起こしているのでは?と思ったのだが、その場合は50日間近く継続して動かさないと発生しないはずで、30分ぐらいでオーバーフローするはずがない。

どうも分からないので、以下のようなテストプログラムを組んで走らせてみた。time.clock()がオーバーフローを起こしているなら、戻り値がマイナスになってプログラムが停止するはず。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import time

if __name__ == "__main__":
    print time.ctime()
    try:
        count = 1000
        while True:
            tc = time.clock()
            tt = time.time()
            if(tc < 0):
                print tc, tt
                print time.ctime()
                break
            count = count - 1
            if count <= 0:
                print tc, tt
                count = 1000
            for i in range(1, 10000):
                pass
    except KeyboardInterrupt:
        print '\nbreak'

結果はこうなった。驚いた事に、本当にtime.clock()がマイナスの値を返している。エポック秒を返すtime.time()は正しい値を返しているので、time.clock()だけおかしい理由が分からない。

$ python TimeTest.py
Sat Mar 16 17:38:15 2013
10.28 1363423105.5
20.35 1363423115.61
30.42 1363423125.71
40.49 1363423135.83
50.55 1363423145.93
60.63 1363423156.05
70.69 1363423166.15
80.76 1363423176.26
90.83 1363423186.37
100.9 1363423196.47

中略

2053.65 1363425156.1
2063.71 1363425166.19
2073.77 1363425176.29
2083.83 1363425186.38
2093.89 1363425196.47
2103.95 1363425206.56
2114.01 1363425216.65
2124.07 1363425226.75
2134.13 1363425236.84
2144.2 1363425246.94
-2147.477296 1363425250.24
Sat Mar 16 18:14:10 2013

マイナスになった瞬間の値は -2147.477296 で、これは32bit整数型の最大値 2147483647 と似た値なのが非常に気になる。time.clock()はプロセッサクロックタイムを返す関数なので、Pythonの問題ではなくRaspberry Piのハードウェアに特有の問題という気がする。

それとも何か根本的に勘違いしているのだろうか?

ロータリーエンコーダ

RPi.GPIOを使った外部機器からの入力。押しボタンスイッチの他に何か無いか?と思って探すとArduinoで使ったロータリーエンコーダが出てきたので、これを試してみる。
秋月で取り扱っている2相出力のエンコーダで、ツマミを回すとA相、B相の各端子からパルスが出力される。パルスはA相とB相でずれて出力されるようになっていて、回転方向によってずれかたが異なるから、それでどちらに回されたのかを知ることができる。

このロータリーエンコーダはクリックのある場所で停止するようになっていて、停止位置では端子がどこにも接続されていない開放状態となるので、抵抗でプルアップしておく必要がある。ブレッドボードで使い勝手が良いように、小型ユニバーサル基板を使ってプルアップ抵抗を予め付けておいた。

R0011719回転方向の検出はA相、B相の状態にそれぞれ番号を付けておき、状態が変化した時に新しい番号と以前の番号を比較して、増加していれば右回転(C.W)、減少していれば左回転(C.C.W)と判断する。この時、3→0と0→3は別にチェックしておく。

rotary_chart

プログラムは以下の通り。
プッシュスイッチの時と同じように変化点からのディレイでチャタリングを除去しているが、ディレイを長くするとツマミを高速で回した時に取りこぼしが発生してしまうので、時間を短くしてある。
プッシュスイッチよりはチャタリングの発生時間が短いらしく、これでも問題はなかった。ちなみに時間を計る為に使用している time.clock() はPythonのバージョン3.3で取り除かれてしまったので、3.3で試す時は別の時間計測関数を使う必要がある。 ※

※2013/03/16追記 : time.clock()が正しい値を返さない場合がある(原因不明)事が分かったので、time.time()に変更

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import RPi.GPIO as GPIO
import time

class pin:
    def __init__(self, port):
        self.port = port
        self.dt = time.time()
        GPIO.setup(self.port, GPIO.IN, GPIO.PUD_OFF)
        self.stat = GPIO.input(self.port)
    def get(self):
        stat = GPIO.input(self.port)
        dt = time.time()
        if stat != self.stat and dt > self.dt + 0.01:
            self.stat = stat
            self.dt = dt
        return self.stat        

class rotary:
    def __init__(self, port_a, port_b, val, lo, hi):
        self.ports = [pin(port_a), pin(port_b)]
        self.val = val
        self.lo = lo
        self.hi = hi
        self.stat = 0
        self.sa = self.ports[0].get()
        self.sb = self.ports[1].get()
    def get(self):
        sa = self.ports[0].get()
        sb = self.ports[1].get()
        if sa != self.sa or sb != self.sb:
            self.sa = sa
            self.sb = sb
            if sa == GPIO.HIGH:
                if sb == GPIO.HIGH:
                    stat = 0    # H/H
                else:
                    stat = 3    # H/L
            else:
                if sb == GPIO.HIGH:
                    stat = 1    # L/H
                else:
                    stat = 2    # L/L
            if stat - self.stat == 1 or self.stat - stat == 3:    # RIGHT
                if self.val < self.hi:
                    self.val = self.val + 1
            elif self.stat - stat == 1 or stat - self.stat == 3:  # LEFT
                if self.val > self.lo:
                    self.val = self.val - 1
            self.stat = stat
        return self.val

if __name__ == "__main__":
    GPIO.setmode(GPIO.BCM)
    val = 25
    re = rotary(24, 25, val, 0, 50)
    try:
        while True:
            v = re.get()
            if(val != v):
                print '%02d' % v,'-' * (v - 1),'*','-' * (51 - v)
                val = v
    except KeyboardInterrupt:
        print '\nbreak'
        GPIO.cleanup()

ルート権限でプログラムを実行し、ロータリーエンコーダのつまみを左右に動かすと。以下のように表示されるはず。
rotary_out

チャタリング除去時間を短くしても、高速でつまみを回すと取りこぼしが発生する場合があったので、Pythonではこれ以上は難しいかも知れない。C言語を使って直接GPIOにアクセスすればもっと確実にデータを取得できるが、高速で回した時に1パルス分の取りこぼしも許されない用途というのはあまり考えられないし、その場合は別な手段を考えるべきだろう。

チャタリング除去

RPi.GPIOで add_event_detect を使うと入力ポートの変化点を検出できるので便利なのだが、接点式スイッチは押された瞬間に細かくOn/Offが変化するチャタリング現象が発生するため、それもすべて検出されてしまい都合が悪い。

チャタリング除去は抵抗やコンデンサを使って回路的に行う方法と、ソフトウェア的に行う方法があって、ここではソフトウェア的に除去する方法を試してみる。

プログラムは以下の通り

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import RPi.GPIO as GPIO
import time

class button:
    def __init__(self, port, stat = GPIO.LOW, pud = GPIO.PUD_OFF):
        self.port = port
        self.stat = stat
        self.detect = False
        self.dt = time.time()
        GPIO.setup(self.port, GPIO.IN, pud)
    def get(self):
        stat = GPIO.input(self.port)
        dt = time.time()
        if stat != self.stat and dt > self.dt + 0.02:
            self.stat = stat
            self.dt = dt
            self.detect = True
        return self.stat        
    def check(self):
        stat = self.get()
        detect = self.detect
        self.detect = False
        return stat,detect

if __name__ == "__main__":
    GPIO.setmode(GPIO.BCM)
    btns = [button(17, GPIO.LOW, GPIO.PUD_DOWN),
            button(22, GPIO.LOW, GPIO.PUD_DOWN)]
    try:
        while True:
            for btn in btns:
                stat,detect = btn.check()
                if(detect):
                    if(stat == GPIO.LOW):
                        print btn.port,'FALLING'
                    else:
                        print btn.port,'RISING'
    except KeyboardInterrupt:
        print '\nbreak'
        GPIO.cleanup()

除去する方法は色々と考えられるが、最初の変化点を検出してから一定時間は変化をチェックしない単純な方法でやってみた。時間の経過はCPUクロックタイムをカウントする time.clock() を使用。 ※
どのぐらい待てば確実にチャタリング除去できるのかは良く分からなかったので、実測して値を設定したが、場合によっては調整する必要があるかも知れない。

※2013/03/16追記 : time.clock()が正しい値を返さない場合がある(原因不明)事が分かったので、time.time()に変更

GPIOコールバック処理

RPi.GPIOはルート権限が必要なため、CGIで使うには不便なのでWiringPiを試してみたが、それ以外の用途でならapt-getで簡単にインストールできるし、有用性は高い。

現在進行形で開発中のパッケージなので、バージョンが上がると関数が追加されたり無くなったりと変化が大きく注意が必要ではあるが、Raspberry PiでGPIO入出力を行う場合はこちらが標準になるのだろう。

ただし、RPi.GPIOのプロジェクトページにも書いてある通りPythonはガベージコレクションが発生した場合に遅延が生じる可能性があり、そもそもリアルタイム性をあまり考慮しないLinuxカーネル下で動作するので、クリチカルなタイミングを必要とする処理には向かないことを覚えておく必要がある。
そういう用途にはArduinoの出番だ。

RPi.GPIOの最新バージョンは0.5.0aで、入力ポートの変化によるコールバック処理が追加されている。これは入力ポートが変化した時に定義したコールバック関数を呼び出す機能で、別スレッドで処理されるから用途によっては便利。

GPIOポート17(11番ピン)に押しボタンスイッチをつなぎ、押した時に+3.3Vに通電するようにする。間違えて+5Vに通電してしまうとRaspberry Piが壊れる可能性があるので注意。以前スイッチをつないだときはプルダウン抵抗で押していない時はGNDになるようにしたが、今回は内蔵のプルアップ/プルダウン機能を試すので何もつながない。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import RPi.GPIO as GPIO
import time

def event_callback1():
    print 'event_callback1'

if __name__ == "__main__":
    print 'GPIO Version',GPIO.VERSION
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO17 Pin11
    GPIO.add_event_detect(17, GPIO.RISING)
    GPIO.add_event_callback(17, event_callback1)
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print '\nbreak'
        GPIO.cleanup()

プログラムを走らせてボタンを押すと、押した時だけコールバック関数が呼び出される。押した時ではなく離した時に呼び出したい場合は、GPIO.add_event_detectでGPIO.FALLING、両方で呼び出したい場合はGPIO.BOTHを指定すれば良い。

ここまでは良いのだが、スイッチを増やしてそれぞれにコールバック処理を行いたい場合は上手く行かない。以下のようにGPIO22にスイッチを追加して同じようにコールバック関数を定義しても、全く呼び出されないのだ。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import RPi.GPIO as GPIO
import time

def event_callback1():
    print 'event_callback1'
def event_callback2():
    print 'event_callback2'

if __name__ == "__main__":
    print 'GPIO Version',GPIO.VERSION
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO17 Pin11
    GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO22 Pin15
    GPIO.add_event_detect(17, GPIO.RISING)
    GPIO.add_event_detect(22, GPIO.RISING)
    GPIO.add_event_callback(17, event_callback1)
    GPIO.add_event_callback(22, event_callback2)
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print '\nbreak'
        GPIO.cleanup()

どうやらRPi.GPIOのコールバック処理用スレッドは、ひとつのポートに対してだけしか有効ではないらしい。その代わりGPIO.add_event_callbackを複数回呼び出してひとつのポートに対して複数のコールバック処理を定義することはできる。
でもこれはあまり意味がないような気がする。ひとつにまとめてしまえば良いわけだし。何か意味があるのだろうか?