はじめに
Maker Faire Tokyo 2023に出展しました!
展示物のうちの「射的2.0」を、「🔫ピストル編」「🎯まと編」「⚙️システム編」「🖥️GUI編」の4本にわけてご紹介します。
この記事は「⚙️システム編」です。
全体像
やりたいこと
- 30秒間、LEDをランダムに点灯させる
- 点灯中のLEDとセットの赤外線受信モジュールにセンサー値を受信した場合に、得点を加算し(画像③)、 音を鳴らす
- 最終得点をkintoneに保存する(画像④)
準備するもの
- Raspberry Pi 3 Model B
- 赤外線リモコン受信モジュール(OSRB38C9AA)
- フレキシブルLED(CY-FRX3002-M2001-Y-E)
- kintone環境
kintoneアプリ
コーディング前にkintoneアプリを用意しておきます。
フィールド項目 | フィールド名 | フィールドコード |
---|---|---|
文字列(1行) | プレイヤー名 | name |
数値 | 得点 | points |
kintone APIトークンの生成
作成したkintoneのアプリ設定画面からAPIトークンを生成します。 レコードの登録に権限をつけて、アプリを更新しておきます。 その際に、生成されるAPIトークンをコピーしておきます。
配線(イメージ図)
役割 | ピン番号 |
---|---|
VCC | 2 |
GRAND | 9 |
OUT | 8, 10, 11, 12, 13 |
LED | 15, 16, 18, 22, 29 |
実装
このコードでは、GPIO.BCM
を用いたピンを指定しています。
「GPIO.BCMとは?」と思った方は、コード以下の「GPIOのモードに注意」をご確認ください。
import RPi.GPIO as GPIO import time import random import requests import json from pydub.playback import play POINT = 0 STATUS = True KINTONE_URL = "https://{sample}.cybozu.com/k/v1/record.json" KINTONE_TOKEN = "XXXXXXXXXXX" KINTONE_APP = 1 LED_PINS = [1, 5, 6, 7, 8] IR_PINS = [14, 15, 17, 18, 27] HIT_SOUND = AudioSegment.from_mp3("hit.mp3") GPIO.setmode(GPIO.BCM) for pin in LED_PINS + IR_PINS: GPIO.setup(pin, GPIO.IN if pin in IR_PINS else GPIO.OUT) def check_ir_signal(led_pin, ir_pin, duration): end_time = time.time() + duration while time.time() < end_time: if GPIO.input(ir_pin) == 0: global POINT POINT += 1000 play(HIT_SOUND) GPIO.output(led_pin, GPIO.LOW) return True return False def post_kintone(data): headers = { "X-Cybozu-API-Token": KINTONE_TOKEN, "Content-Type": "application/json" } payload = { "app": KINTONE_APP, "record": data } response = requests.post(KINTONE_URL, headers=headers, data=json.dumps(payload)) return response.json() def main_game_loop(): one_play_time = time.time() + 30 while time.time() < one_play_time: print("-----total point-----") print(POINT) led_pin, ir_pin = random.choice(list(zip(LED_PINS, IR_PINS))) led_duration = random.choice([2, 3]) GPIO.output(led_pin, GPIO.HIGH) if check_ir_signal(led_pin, ir_pin, led_duration): time.sleep(0.5) else: GPIO.output(led_pin, GPIO.LOW) print("*****Game Over!*****") data = { "name": { "value": NAME }, "points": { "value": POINT } } response = post_kintone(data) print(response) try: while STATUS: time.sleep(1) main_game_loop() except KeyboardInterrupt: GPIO.cleanup()
GPIOのモードに注意
- GPIOのsetmodeは2種類ある
- GPIO.BOARD
- ピン番号を指定する方法
- GPIO.BCM
- GPIO番号を指定する方法
- BCM: Broadcom SOC channel
- 参考リンク
コード解説
使用するライブラリをインポートします。
import RPi.GPIO as GPIO import time import random import requests import json from pydub.playback import play
変数を定義しておきます。
POINT = 0 # 得点 STATUS = True # ゲームステータス True:プレイ中、 False:終了中 KINTONE_URL = "https://{sample}.cybozu.com/k/v1/record.json" #サブドメインを変更する KINTONE_TOKEN = "XXXXXXXXXXX" #生成したAPIトークン KINTONE_APP = 1 #作成したアプリ番号 LED_PINS = [1, 5, 6, 7, 8] IR_PINS = [14, 15, 17, 18, 27] hit_sound = AudioSegment.from_mp3("hit.mp3") #ファイルと同じディレクトリに音声ファイルを置いておく
GPIOを設定します。下記のコードで、[1,14],[5,15]...といった、[LED, IR]の組み合わせの配列を用意しています。
GPIO.setmode(GPIO.BCM) for pin in LED_PINS + IR_PINS: GPIO.setup(pin, GPIO.IN if pin in IR_PINS else GPIO.OUT)
今回のサンプルコードでは、関数を3つ作成しています。
1. 引数で指定された時間内に赤外線センサからの信号を確認する関数
引数で指定された特定の時間を繰り返し、IRピンの信号をチェックします。信号が1の場合は未受信、0の場合は赤外線を受信したと判断し、ポイントを加算しTrueを返します。時間を過ぎた場合は、Falseを返します。
# @params # led_pin: LEDピン番号 # ir_pin: IRピン番号 # duration: 点灯時間 def check_ir_signal(led_pin, ir_pin, duration): end_time = time.time() + duration while time.time() < end_time: if GPIO.input(ir_pin) == 0: global POINT POINT += 1000 #1000点加点する play(HIT_SOUND) #音を流す GPIO.output(led_pin, GPIO.LOW) #LEDを消灯 return True return False
2. kintoneにデータをPOSTする関数
requestモジュールを使用して、kintoneにレコードを1件追加します。
# kintoneにデータをPOSTする関数 # data: recordの中身 def post_kintone(data): headers = { "X-Cybozu-API-Token": KINTONE_TOKEN, "Content-Type": "application/json" } payload = { "app": KINTONE_APP, "record": data } response = requests.post(KINTONE_URL, headers=headers, data=json.dumps(payload)) return response.json()
3. 30秒間繰り返す(メインループ)関数
1プレイ30秒と設定しているため、30秒間繰り返す関数です。 ランダムなLEDを点灯させ、先ほど解説した1つ目の関数を呼び出します。赤外線受信をしていればTrueを受け取り、0.5秒sleepの時間をとります。Falseを受け取った場合は、点灯中のLEDを消灯します。
30秒経過したら1プレイ終了と判断し、post_kintone関数を呼び出して、kintoneに得点を登録します。
def main_game_loop(): one_play_time = time.time() + 30 #30秒を指定 while time.time() < one_play_time: print("-----total point-----") print(POINT) led_pin, ir_pin = random.choice(list(zip(LED_PINS, IR_PINS))) led_duration = random.choice([2, 3]) # LED点灯時間 GPIO.output(led_pin, GPIO.HIGH) # ランダムなLEDを点灯 if check_ir_signal(led_pin, ir_pin, led_duration): time.sleep(0.5) else: GPIO.output(led_pin, GPIO.LOW) print("*****Game Over!*****") data = { "name": { "value": NAME }, "points": { "value": POINT } } response = post_kintone(data) print(response)
最後に、関数呼び出しと、例外処理を追加しています。
※ 本来STATUSの値は、別の記事で紹介するUI部分と組み合わせてTrue/Falseを切り替えています。しかし、この記事ではSTATUSを変更しないため無限ループが発生します。
try: while STATUS: time.sleep(1) #無限ループ負荷軽減のため main_game_loop() # ctrl + C などで処理を停止した際に実行される except KeyboardInterrupt: GPIO.cleanup()
動作確認
python3 filename.py
を実行します。
LEDがランダムに点灯、赤外線受信した時に加算、30秒後にkintoneにデータが登録されていれば動作チェックはOKです。
まとめ
簡易的ですが、動作検証をすることができました。 本番では、紹介したコードにUI部分を加えて、マルチプロセスで複数のタスクを実行するように修正を加えています。 Pythonのマルチプロセスは意外と簡単に実装できたのですが、グローバル変数を使用してプロセス同士の値をやり取りしようとすると結構ハマるため、プロセス実行時に引数で渡して回避しています。