加速度センサ

以前にArduinoで使ってみた加速度センサのインタフェースがI2Cなので、これもRaspberry Piに接続して試してみる事にした。加速度センサはフリースケール社のMMA7455Lを14ピンのDIP基板に乗せたものだ。ストロベリーリナックスで購入できる。

動作電圧が2.4V~3.6Vで、5Vでは動作しないから5V動作のArduinoで使う時は注意が必要なのだが、Raspberry PiはGPIOが3.3Vなので都合が良い。まずはブレッドボード上でVCC/GND/SDA/SCLの各ピンをGPIOの対応するピンに接続。
MMA7455

MMA745Lモジュール

Raspberry Pi

1 : GND

6 : GND

2 : VDD  6 : AVDD  8 : CS

1 : 3.3V

13 : SDA

3 : SDA

14 : SCL

5 : SCL

Raspberry PiのI2Cポートにはプルアップ抵抗が付いているので、プルアップする必要は無いはずなのだが、抵抗を外付けしないと正しく認識されなかった。正しく認識されていれば以下のように検出される。

>pi@raspberrypi ~ $ sudo i2cdetect 1
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-1.
I will probe address range 0x03-0x77.
Continue? [Y/n]
0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- 1d -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

あとはPythonで簡単なプログラムを書いて確認。加速度センサの値は簡単に取得できたので、ついでに値をPyGameでグラフ表示できるようにしてみた。プログラムは以下の通り。

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

import sys
import pygame
from pygame.locals import *
import smbus
import time

screen_rect = Rect(0, 0, 640, 320)
white = 255, 255, 255
red = 255, 0, 0
green = 0, 255, 0
blue = 0, 0, 255
yellow = 255, 255, 0

class MMA7455L:
    i2c = smbus.SMBus(1)   
    addr = 0x1d
    def __init__(self):
        self.i2c.write_byte_data(self.addr, 0x16, 0x05)     # 2G/Measurement Mode
        for i in range(0, 6):
            self.i2c.write_byte_data(self.addr, 0x10 + i, 0)
        time.sleep(0.1)
    def get(self):
        return self.i2c.read_i2c_block_data(self.addr, 0x06, 3)
    def drift(self):
        dd = [0, 0, 0]
        for i in range(0, 20):
            buf = self.i2c.read_i2c_block_data(self.addr, 0x06, 3)
            dd[0] = dd[0] + signed(buf[0])
            dd[1] = dd[1] + signed(buf[1])
            dd[2] = dd[2] + signed(buf[2]) - 64
        dd[0] = (dd[0] / 8) * -1
        dd[1] = (dd[1] / 8) * -1
        dd[2] = (dd[2] / 8) * -1
        for i in range(0, 3):
            self.i2c.write_byte_data(self.addr, 0x10 + i * 2, dd[i])
            self.i2c.write_byte_data(self.addr, 0x11 + i * 2, (dd[i] >> 8) & 0x07)
        time.sleep(0.1)

def signed(n):
    return n if n < 128 else n - 256 

if __name__ == "__main__":
    mma = MMA7455L()
    mma.drift()
    pygame.init()
    screen = pygame.display.set_mode(screen_rect.size)
    center = screen_rect.height / 2
    pygame.draw.line(screen, blue, (20, center),\
                    (screen_rect.width, center), 1)
    pygame.draw.line(screen, blue, (20, center - 64),\
                    (screen_rect.width, center - 64), 1)
    pygame.draw.line(screen, blue, (20, center + 64),\
                    (screen_rect.width, center + 64), 1)
    pygame.draw.line(screen, blue, (20, center - 128),\
                    (screen_rect.width, center - 128), 1)
    pygame.draw.line(screen, blue, (20, center + 128),\
                    (screen_rect.width, center + 128), 1)
    pygame.draw.line(screen, blue, (20, 0), (20, screen_rect.height), 1)
    font = pygame.font.Font(None, 16)
    text = font.render("+2G", True, white)
    screen.blit(text, (0, center - 133))
    text = font.render("+1G", True, white)
    screen.blit(text, (0, center - 69))
    text = font.render(" 0G", True, white)
    screen.blit(text, (0, center - 5))
    text = font.render("-1G", True, white)
    screen.blit(text, (0, center + 59))
    text = font.render("-2G", True, white)
    screen.blit(text, (0, center + 123))
    d1 = mma.get()
    d1[0] = signed(d1[0]) * -1
    d1[1] = signed(d1[1]) * -1
    d1[2] = signed(d1[2]) * -1
    x = 20
    try:
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            d2 = mma.get()
            d2[0] = signed(d2[0]) * -1
            d2[1] = signed(d2[1]) * -1
            d2[2] = signed(d2[2]) * -1
            pygame.draw.line(screen, red, (x, d1[0] + center),\
                            (x + 1, d2[0] + center), 1)
            pygame.draw.line(screen, green, (x, d1[1] + center),\
                            (x + 1, d2[1] + center), 1)
            pygame.draw.line(screen, yellow, (x, d1[2] + center),\
                            (x + 1, d2[2] + center), 1)
            pygame.display.update()
            x = x + 1
            d1 = d2
            time.sleep(0.1)
    except KeyboardInterrupt:
        print '\nbreak'
        pygame.quit()

実行すると以下のように表示される、グラフの赤線がX軸、緑線がY軸、黄色線がZ軸。Z軸が最初から1Gになっているのは、何もしなくても重力が加わっているからだ。グラフの最初は加速度センサを傾けた時、後半はそれぞれの軸に対して振ったときの値を示している。
MMATest プルアップ抵抗の謎はあったが、I2C液晶の時よりも簡単に使う事ができた。モジュールは千円以下で買えるので、手軽に色々な応用ができそうだ。

広告

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があまり速くないので今風のゲームは無理だろうけど、チュートリアルサイトを見るとスーパーファミコン時代のゲームなら作成可能という説明があって、確かにそんな感じがする。