kintoneGeeks blog

kintoneに関連する情報を発信しています

【Maker Faire Tokyo 2023 展示作品】射的2.0 ⚙️システム編

はじめに

Maker Faire Tokyo 2023に出展しました!
展示物のうちの「射的2.0」を、「🔫ピストル編」「🎯まと編」「⚙️システム編」「🖥️GUI編」の4本にわけてご紹介します。

この記事は「⚙️システム編」です。

全体像

システム図.png

やりたいこと

  • 30秒間、LEDをランダムに点灯させる
  • 点灯中のLEDとセットの赤外線受信モジュールにセンサー値を受信した場合に、得点を加算し(画像③)、 音を鳴らす
  • 最終得点をkintoneに保存する(画像④)

準備するもの

kintoneアプリ

コーディング前にkintoneアプリを用意しておきます。

フィールド項目 フィールド名 フィールドコード
文字列(1行) プレイヤー名 name
数値 得点 points

kintone APIトークンの生成

作成したkintoneのアプリ設定画面からAPIトークンを生成します。 レコードの登録に権限をつけて、アプリを更新しておきます。 その際に、生成されるAPIトークンをコピーしておきます。

kintoneのAPIトークンを生成
kintoneのAPIトークンを生成

配線(イメージ図)

役割 ピン番号
VCC 2
GRAND 9
OUT 8, 10, 11, 12, 13
LED 15, 16, 18, 22, 29

IR20230913_ブレッドボード.png

実装

このコードでは、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のモードに注意

コード解説

使用するライブラリをインポートします。

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です。

gif

まとめ

簡易的ですが、動作検証をすることができました。 本番では、紹介したコードにUI部分を加えて、マルチプロセスで複数のタスクを実行するように修正を加えています。 Pythonのマルチプロセスは意外と簡単に実装できたのですが、グローバル変数を使用してプロセス同士の値をやり取りしようとすると結構ハマるため、プロセス実行時に引数で渡して回避しています。