PyQt5 を使ったひらがなドリル用にボタンを自作した

touch-sp.hatenablog.com
f:id:touch-sp:20200823123357p:plain:w400
f:id:touch-sp:20200824222655j:plain:w400

はじめに

Arduino Leonardo(またはArduino Micro)を使ったら簡単にできたと思うが、使っていないArduino Nano Everyがあったので今回はそちらを使用した。
PyQt5を使ったシリアル通信でつまづいた。原因不明のエラーに悩まされたが以下を追加したら解決した。

import serial
with serial.Serial('COM5', 9600) as ser:
    time.sleep(2)

なぜかいったんpyserialを使って接続を確立させるとうまくいった。

バージョン確認(pip freeze)

PyQt5==5.15.0
PyQt5-sip==12.8.0
pyserial==3.4
python-vlc==3.0.11115

Pythonスクリプト

import os
import glob
import random
import time

import sys
from PyQt5 import QtCore, QtSerialPort
from PyQt5.QtWidgets import QLabel, QApplication, QWidget, QFrame
from PyQt5.QtGui import QImage, QPixmap, QFont

import vlc

import serial
with serial.Serial('COM5', 9600) as ser:
    time.sleep(2)

with open('kana.txt', encoding='utf-8') as f:
    all_kanas = set(f.read().replace('\n',''))

for i in range(4):
    button_image = [QImage('button/%d.png'%i) for i in range(4)]

ok_sound = vlc.Media('sound_kana/ok.wav')
boo_sound = vlc.Media('sound_kana/boo.wav')
player = vlc.MediaPlayer()

dummy_sound = vlc.Media('sound_kana/dummy.wav')
player.set_media(dummy_sound)
player.play()

os.chdir(os.path.join(os.getcwd(), 'character'))
all_character = [os.path.splitext(i)[0] for i in glob.glob('*.png')]

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.answer_position = 0
        self.button_ready = False
        
        self.serial = QtSerialPort.QSerialPort(
            'COM5', baudRate=QtSerialPort.QSerialPort.Baud9600, 
            readyRead = self.receive)
        self.serial.open(QtCore.QIODevice.ReadWrite)

        self.initUI()
        
    def initUI(self):

        self.kana_label = [QLabel(self) for i in range(10)]
        for i in range(10):
            self.kana_label[i].setGeometry(600+i*100, 200, 100, 100)       
            self.kana_label[i].setFont(QFont("Times", 72, QFont.Bold))
            self.kana_label[i].setAlignment(QtCore.Qt.AlignCenter)
            self.kana_label[i].setLineWidth(3)
        
        self.button_label = [QLabel(self) for i in range(4)]
        self.choice_label = [QLabel(self) for i in range(4)]
        for i in range(4):
            self.button_label[i].setGeometry(184 + 434 * i, 900, 250, 50)
            self.button_label[i].setPixmap(QPixmap.fromImage(button_image[i]))

            self.choice_label[i].setGeometry(184 + 434 * i, 650, 250, 250)
            self.choice_label[i].setFont(QFont("Times", 120, QFont.Bold))
            self.choice_label[i].setAlignment(QtCore.Qt.AlignCenter)

        self.pic_label = QLabel(self)
        self.pic_label.setGeometry(50, 50, 500, 500)

        self.make_question()

    def receive(self):
        
        #ボタンが無効なら何もしない
        if self.button_ready == False:
            dummy = self.serial.readAll()
        else:
            self.button_ready = False
            my_message = self.serial.readAll().data()[0]
            
            if ['A', 'S', 'D', 'F'].index(chr(my_message)) == self.answer_position:
                
                player.set_media(ok_sound)
                player.play()

                loop = QtCore.QEventLoop()
                QtCore.QTimer.singleShot(1000, loop.quit)
                loop.exec_()

                self.make_question()
            
            else:
                player.set_media(boo_sound)
                player.play()

                loop = QtCore.QEventLoop()
                QtCore.QTimer.singleShot(1000, loop.quit)
                loop.exec_()

                self.button_ready = True

    def make_question(self):
        #画面のリセット
        for i in range(10):
            self.kana_label[i].setFrameStyle(QFrame.NoFrame)
            self.kana_label[i].setText('')
        character_name = random.choice(all_character)
        star_image = QImage(character_name + '.png')
        self.pic_label.setPixmap(QPixmap.fromImage(star_image))
        kanas = list(character_name)
        kana_len = len(kanas)
        question_position = random.randint(0, kana_len-1)
        question_kana = character_name[question_position]
        kanas[question_position] = ''

        for i in range(kana_len):
            self.kana_label[i].setText(kanas[i])
            
        self.kana_label[question_position].setFrameStyle(QFrame.Box | QFrame.Plain)
        others = list(all_kanas - set([question_kana]))
        choices = random.sample(others, 3)
        choices.append(question_kana) 
        random.shuffle(choices)
        self.answer_position = choices.index(question_kana)

        for i in range(4):
            self.choice_label[i].setText(choices[i])

        self.button_ready = True

    def keyPressEvent(self, e):
            
        # エスケープキーを押すと画面が閉じる
        if e.key() == QtCore.Qt.Key_Escape:
            self.close()

app = QApplication(sys.argv)
ex =Window()
p = ex.palette()
p.setColor(ex.backgroundRole(), QtCore.Qt.white)
ex.setPalette(p)
ex.showFullScreen()
sys.exit(app.exec_())

Arduinoスケッチ

const int buttonON = LOW;    // ボタンが押されているとピンの値はLOW
const int buttonOFF = HIGH;  // ボタンが押されていないとピンの値はHIGH

const int buttonPin1 = 2;
const int buttonPin2 = 3;
const int buttonPin3 = 4;
const int buttonPin4 = 5;

int buttonState1;

void setup() {
  Serial.begin(9600);
  pinMode(buttonPin1, INPUT_PULLUP);
  pinMode(buttonPin2, INPUT_PULLUP);
  pinMode(buttonPin3, INPUT_PULLUP);
  pinMode(buttonPin4, INPUT_PULLUP);
}

void loop() {
  if (digitalRead(buttonPin1) == buttonON) {
    Serial.write('A');  
    delay(300);
  }
  if (digitalRead(buttonPin2) == buttonON) {
    Serial.write('S');  
    delay(300);
  }
  if (digitalRead(buttonPin3) == buttonON) {
    Serial.write('D');  
    delay(300);
  }
  if (digitalRead(buttonPin4) == buttonON) {
    Serial.write('F');  
    delay(300);
  }
}

使用したもの

Arduino Nano Every
ブレッドボード
Cherry MXキースイッチ(青軸)
キーキャップ( 「遊舎工房」の通販で購入)
タミヤ	PLA PLATE 1.0mm厚
タミヤ プラ材 5mm角棒
タミヤ	1.5A 平行コード (5m)
DAISO	カラーボード(白)450×300×厚さ5mm
DAISO	PETIT BLOCK(ウェディングケーキ)