気の向くままに辿るIT/ICT/IoT
webzoit.net
IoT・電子工作

PyQt5/Qt Designerで自作スマートスピーカー用操作パネルを作成

ウェブ造ホーム前へ次へ
サイト内検索
カスタム検索
Raspberry Piって?

PyQt5/Qt Designerで自作スマートスピーカー用操作パネルを作成

PyQt5/Qt Designerで自作スマートスピーカー用操作パネルを作成

2019/11/07

 以前、作って運用しつつもブラッシュアップ中のRaspberry Pi 3 Model B+とJuliusOpen JTalkベースの自作スマートスピーカーがあります。

 主な機能は、

 尚、ラズパイ用ACアダプタを挿したスイッチ付きコンセントでのON/OFFとは別にラズパイ用boot/reboot/shutdown物理ボタン付き。

 ちなみに便利なのでラズパイだけでなく、PC/Debianにも自作スマートスピーカー機能を搭載しています。

PyQt5/Qt Designerで操作パネルを作成

 今回は、ラズパイにはモニタを搭載していないものの、スマートスピーカーを載せたノートPC(OS:Debian)用、また、このノートからラズパイのスマートスピーカー機能をマウスで遠隔操作すべく、GUIツールキットであるPyQt5/Qt Designerでスマートスピーカー用の操作パネルを作ってみることにしました。

 前回、Gtk+ 3で作ってみたのですが、PyGtkの情報はそこそこあるもデザイナーソフトGladeの情報が少なく、tabウィジェットの作り方に詰まり、他を探してみた次第です。

 が、Qtの方は、Qt Designerの情報は結構ある上、直感的にレイアウトでき、親しみやすさがある一方、PyQt5の(PyQt4/PyQt3含めても)情報が少なく、一長一短...。

 ただ、Designerの情報があれば、PyQt5の情報が少なかったとしてもPython自体の文法で、なんとかなるだろうということでPyGtk/GladeからPyQt5/Qt Designer(Qt5Designer)に乗り換えることにしました。

 ちなみに自身は、Pythonを一通り身につけているわけではないものの、より身近なPerl版は、更新が活発でなかったり、情報が少なかったり、Rubyは門外漢、コンパイルが必要なC/C++よりはインタプリタ...という消去法の結果ながら、機械学習、ディープラーニング、AIとも相性が良さそうという理由もあってPythonバインディングを選択しました。

 ありもののGUIを見ていた限り、ユーザー視点からはGtkの方が好みでQtはちょっと...と思っていたのですが、作ってみるとそんな差は全く感じないルック&フィールに、なんで出来合いのものは、差を感じるのか不思議...。

 今尚活発に開発が進んでいるという点では、ある程度、限られるものの、そもそもGTKやQtだけでなく、GUIツールキットは、クロスプラットフォーム対応のものだけでも結構あるようです。

 尚、PyQt5は、商用利用の場合は、有料、個人利用では、無償で利用が認められているようです。

環境

$ sudo apt update
$ sudo apt -y upgrade
$ sudo apt -y install qttools5-dev-tools qt5-default pyqt5-dev-tools
...
$

 自身はOSにDebian( GNU/Linux)を使っており、pip/pip3やapt/apt-getどちらもいけるようですが、前者でつまづいたので後者で環境を用意することにしました。

 尚、パッケージ名は、直感的とは言い難く、若干試行錯誤した結果、パッケージ[qttools5-dev-tools]があれば、PyQt5自体のコードは書け、これにパッケージ[qt5-default]を追加インストールすると[designer](Qt5Designer用コマンド)も、更に[pyqt5-dev-tools]をインストールすると[pyuic5](Qt Designerで生成される.uiファイルをpythonコードに変化するコマンド)も使えるようになりました。

$ designer &
Qt Designer/Qt Linguist/Qt Assistant

 QtDesigner/Qt5Designerを起動するには、[designer]コマンドを実行します。(Debianではインストールした時点でPATHが通ってました。)

 気づけば、デスクトップのメニューには、Qt Designerの他、Qt Linguist、Qt Assistantなるものも登録されていたので、併せて起動してスクリーンショットを撮ってみました。

 [designer]コマンドの実行でQt Linguist、Qt Assistantが一緒に起動するわけではないのであしからず。

$ pyuic5 test.ui -o test.py
$

 QtDesigner/Qt5Designerで作成したtest.uiファイルを[pyuic5]コマンドでtest.pyに変換するには、こんな感じで実行します。

 この方法だと.pyファイルに反映させたい場合、Qt Designerで編集・変更する度に変換作業を要する[pyuic5]での変換が必要になります。

$ cat test1.py
#!/usr/bin/python3
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtWidgets import QApplication
 
class ExampleApp(QtWidgets.QMainWindow):
  def __init__(self, parent=None):
    super(ExampleApp, self).__init__(parent)
    self.ui = uic.loadUi("/path/to/test.ui", self)
 
def main():
  app = QApplication(sys.argv)
  form = ExampleApp()
  form.show()
  app.exec_()
 
if __name__ == '__main__':
  main()
$

 一方、PyQt/PyQt5では、[pyuic5]で変換する方法とは別に[uic]モジュールをimportすることで.uiファイルを読み込む方法も用意されています。

 .uiファイルを読み込む方法なら、Qt Designerで保存後にスクリプトを起動させれば、即反映されますし、仮にスクリプトが起動中なら一度終了させ、起動し直せば反映されるのでアジャイル的な作り方をする場合、便利です。

 試作中の自身は、主にこの[uic]モジュールをimportする方法をとっています。

試作

PyQt5/Qt5Designerで作った自作スマートスピーカー用操作パネル

 スマートスピーカーの性質上、時代に逆行している感がなくもないですが、スマートスピーカー機能をデスクトップアプリとしても使うことができると便利なシーンもあるかなと思っての試作です。

 今回は、試作的にコンボボックスを選択することでYouTubeの音楽系プレイリストやICECASTの音楽系のストリーム、音楽タブとしながらも入れてしまったRadikoの各種ラジオ局を再生、プレイリスト再生のYouTube用にスキップボタン、全ての停止操作を網羅した停止ボタンを配置してみました。

 画像では隠れてしまいましたが、これは、コンボボックス(プルダウンボタンの付いた表示ボックス)を開いたところです。

 また、画像のリストは途中で切れてますが、実際にはもっと選択肢があり、全て動作確認済みです。

 何れにせよ、あとでタブを追加して音楽とRadikoは分けようと思います。

 当初、comboboxの値に応じて再生ボタンで...と思ったのですが、どうもできないっぽかったので選択で再生にしました。

 尚、自身の場合、ラズパイとノートPC共にスマートスピーカー機能を入れているので、コンボボックスと各ボタンをそれぞれ用意しました。

$ chmod +x /path/to/sp_qt_ctrl_panel.py
$ cat /path/to/sp_qt_ctrl_panel.py
#!/usr/bin/python3
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtWidgets import QApplication
import sys
import subprocess
 
class ExampleApp(QtWidgets.QMainWindow):
  def __init__(self, parent=None):
    super(ExampleApp, self).__init__(parent)
    self.ui = uic.loadUi("/path/to/tab_test.ui", self)
 
    channel = ["","JPOP","POPS","JAZZ","CLASSIC","BLUES","JWave","InterFM897","TokyoFM","bayfm78","NACK5","FMヨコハマ","TBSラジオ","ニッポン放送","ラジオ日本","文化放送","ラジオNIKKEI第1","ラジオNIKKEI第2","放送大学","東京NHK第1","東京NHKFM"]
    self.ui.local_channel_combo.addItems(channel)
    self.ui.local_channel_combo.activated[str].connect(self.ui.onLocalActivated)
    self.ui.local_youtube_skip_btn.clicked.connect(self.ui.btnLocalYouTubeSkip)
    self.ui.local_stop_btn.clicked.connect(self.ui.btnLocalStop)
 
    self.ui.rpi_channel_combo.addItems(channel)
    self.ui.rpi_channel_combo.activated[str].connect(self.ui.onRpiActivated)
    self.ui.rpi_youtube_skip_btn.clicked.connect(self.ui.btnRpiYouTubeSkip)
    self.ui.rpi_stop_btn.clicked.connect(self.ui.btnRpiStop)
 
    self.ui.show()
 
  def onLocalActivated(self):
    if self.ui.local_channel_combo.currentText() == "JPOP":
      subprocess.call("/path/to/youtube_jpop_playlist.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "POPS":
      subprocess.call("/path/to/youtube_pops_playlist.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "JAZZ":
      subprocess.call("/path/to/icecastjazz.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "CLASSIC":
      subprocess.call("/path/to/icecastclassical.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "BLUES":
      subprocess.call("/path/to/icecastblues.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "JWave":
      subprocess.call("/path/to/radiko.sh -p FMJ &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "InterFM897":
      subprocess.call("/path/to/radiko.sh -p INT &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "TokyoFM":
      subprocess.call("/path/to/radiko.sh -p FMT &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "bayfm78":
      subprocess.call("/path/to/radiko.sh -p BAYFM78 &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "NACK5":
      subprocess.call("/path/to/radiko.sh -p NACK5 &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "FMヨコハマ":
      subprocess.call("/path/to/radiko.sh -p YFM &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "TBSラジオ":
      subprocess.call("/path/to/radiko.sh -p TBS &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "ニッポン放送":
      subprocess.call("/path/to/radiko.sh -p LFR &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "ラジオ日本":
      subprocess.call("/path/to/radiko.sh -p JORF &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "文化放送":
      subprocess.call("/path/to/radiko.sh -p QRR &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "ラジオNIKKEI第1":
      subprocess.call("/path/to/radiko.sh -p RN1 &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "ラジオNIKKEI第2":
      subprocess.call("/path/to/radiko.sh -p RN2 &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "放送大学":
      subprocess.call("/path/to/radiko.sh -p HOUSOU-DAIGAKU &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "東京NHK第1":
      subprocess.call("/path/to/radiko.sh -p JOAK &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "東京NHKFM":
      subprocess.call("/path/to/radiko.sh -p JOAK-FM &", shell=True)
    else:
      pass
 
  def btnLocalYouTubeSkip(self):
    subprocess.call("/path/to/skip_playlist.sh &", shell=True)
 
  def btnLocalStop(self):
    print("\"Stop Music\" button was clicked")
    subprocess.call("/path/to/stop_radio.sh &", shell=True)
 
  def onRpiActivated(self):
    if self.ui.rpi_channel_combo.currentText() == "JPOP":
      subprocess.call("/path/to/youtube_jpop_playlist_raspi.sh &", shell=True)
    elif self.ui.rpi_channel_combo.currentText() == "POPS":
      subprocess.call("/path/to/youtube_pops_playlist_raspi.sh &", shell=True)
    else:
      pass
 
  def btnRpiYouTubeSkip(self):
    subprocess.call("/path/to/skip_playlist_raspi.sh &", shell=True)
 
  def btnRpiStop(self):
    subprocess.call("/path/to/stop_radio_raspi.sh &", shell=True)
 
def main():
  app = QApplication(sys.argv)
  form = ExampleApp()
  form.show()
  app.exec_()
 
if __name__ == '__main__':
  main()
$ /path/to/sp_qt_ctrl_panel.py
$

 pythonスクリプトはこんな感じです。

 一応、動作確認も。

$ cat /path/to/julius/julius-kits/dictation-kit-v4.4/mysmartspeaker.list | awk 'BEGIN{ printf("%s", "[" ) } NR>=150 && NR<=175 { printf( "\x22%s\x22,", $1) } END { print "]" }'
["JWAVE","InterFM897","TokyoFM","bayfm78","NACK5","FMヨコハマ","TBSラジオ","ニッポン放送","ラジオ日本","文化放送","ラジオNIKKEI第1","ラジオNIKKEI第1","ラジオNIKKEI第2","ラジオNIKKEI第2","放送大学","東京NHK第1","東京NHK第2","東京NHKFM","調布FM","JAZZ","CLASSIC","BLUES","POPS","洋楽","JPOP","邦楽",]
$

 スクリプト中のリストは少ないながら面倒だったので、スマートスピーカーで音声認識させるべく、Juliusで登録してある自前の辞書ファイル(utf-8版)を使ってcatとawkによる、こんな感じのワンライナーで手間を省き、微調整しました。

 日々、つくづく思う(感謝しきりな)ことですが、あらゆる開発環境が揃っていて自由で、何でもやろうと思うことをすぐにできて、セキュアで、異様に動作が遅くなったり、フリーズして強制シャットダウンや再起動せざるを得ない状況に直面することもなく...、UNIX/*BSD/Linuxは、しみじみ良いですね。

 尚、当該スクリプトをデスクトップアイコンから起動したり、メニューに登録する方法、ラズパイのリモート操作などについては、前回のPyGTK/Glade版操作パネルのページにあります。

[2019/11/09]
Radikoと分割した新たな自作スマートスピーカー用操作パネルの音楽タブ

 Radikoタブを追加、音楽タブと分け、ニュースタブ、天気タブも追加、実装しました。

 音楽タブ、Radikoタブ、ニュースタブ、天気タブは、コンボボックスとボタンから成るほぼ同じ構成としました。

 また、音楽タブにおいてスキップボタンが不要なメンバの場合には、当該ボタンを、コンボボックスで空("")以外を選択した場合、停止ボタン押下するまでは、当該コンボボックスを、setEnabled(True)/setEnabled(False)を使って無効にするようにしてみました。

 音楽タブ、Radikoタブ、ニュースタブについては、現在、スクリプトと共に停止ボタン(Push Button)も共有していて、先のコンボの有効・無効化に伴い、これらのコンボも連動するようにしてあります。

 天気タブについては、読み上げだけで計測はしていませんが、数秒〜せいぜい20秒以内と思われ、コンボボックスのみで、そもそもその術を用意していないのですが、停止ボタンは要らないかなとも思いつつも先のコンボの有効・無効化に伴い、これを選択した場合も同様にしてあります。

$ chmod +x /path/to/sp_qt_ctrl_panel_alt.py
$ cat /path/to/sp_qt_ctrl_panel_alt.py
#!/usr/bin/python3
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtWidgets import QApplication
import subprocess
 
class ExampleApp(QtWidgets.QMainWindow):
  def __init__(self, parent=None):
    super(ExampleApp, self).__init__(parent)
    self.ui = uic.loadUi("/path/to/qt5designer/tab_test.ui", self)
 
    channel = ["","JPOP","POPS","JAZZ","CLASSIC","BLUES"]
    self.ui.local_channel_combo.addItems(channel)
    self.ui.local_channel_combo.activated[str].connect(self.ui.onLocalMusicActivated)
    self.ui.local_youtube_skip_btn.clicked.connect(self.ui.btnLocalYouTubeSkip)
    self.ui.local_stop_btn.clicked.connect(self.ui.btnLocalStop)
 
    self.ui.rpi_channel_combo.addItems(channel)
    self.ui.rpi_channel_combo.activated[str].connect(self.ui.onRpiMusicActivated)
    self.ui.rpi_youtube_skip_btn.clicked.connect(self.ui.btnRpiYouTubeSkip)
    self.ui.rpi_stop_btn.clicked.connect(self.ui.btnRpiStop)
 
    radiko = ["","JWave","InterFM897","TokyoFM","bayfm78","NACK5","FMヨコハマ","TBSラジオ","ニッポン放送","ラジオ日本","文化放送","ラジオNIKKEI第1","ラジオNIKKEI第2","放送大学","東京NHK第1","東京NHKFM"]
    self.ui.local_radiko_combo.addItems(radiko)
    self.ui.local_radiko_combo.activated[str].connect(self.ui.onLocalRadikoActivated)
    self.ui.local_stop_btn_2.clicked.connect(self.ui.btnLocalStop)
 
    self.ui.rpi_radiko_combo.addItems(radiko)
    self.ui.rpi_radiko_combo.activated[str].connect(self.ui.onRpiRadikoActivated)
    self.ui.rpi_stop_btn_2.clicked.connect(self.ui.btnRpiStop)
 
    news = ["","Yahoo! Topics","BBC World News","ABC News"]
    self.ui.local_news_combo.addItems(news)
    self.ui.local_news_combo.activated[str].connect(self.ui.onLocalNewsActivated)
    self.ui.local_stop_btn_3.clicked.connect(self.ui.btnLocalStop)
 
    self.ui.rpi_news_combo.addItems(news)
    self.ui.rpi_news_combo.activated[str].connect(self.ui.onRpiNewsActivated)
    self.ui.rpi_stop_btn_3.clicked.connect(self.ui.btnRpiStop)
 
    weather = ["","今日の天気","今日の気温","明日の天気","明日の気温","明後日の天気"]
    self.ui.local_weather_combo.addItems(weather)
    self.ui.local_weather_combo.activated[str].connect(self.ui.onLocalWeatherActivated)
    self.ui.local_stop_btn_4.clicked.connect(self.ui.btnLocalStop)
 
    self.ui.rpi_weather_combo.addItems(weather)
    self.ui.rpi_weather_combo.activated[str].connect(self.ui.onRpiWeatherActivated)
    self.ui.rpi_stop_btn_4.clicked.connect(self.ui.btnRpiStop)
    self.ui.main_menu_btn.clicked.connect(self.ui.btnMainMenu)
    self.ui.aircon_btn.clicked.connect(self.ui.btnAircon)
    self.ui.tv_btn.clicked.connect(self.ui.btnTV)
 
 
    self.ui.show()
 
  def onLocalMusicActivated(self):
    print("\"local_combo\" was selected")
 
    if not (self.ui.local_channel_combo.currentText() == "JPOP" or self.ui.local_channel_combo.currentText() == "POPS"):
      self.ui.local_youtube_skip_btn.setEnabled(False)
    else:
      self.ui.local_youtube_skip_btn.setEnabled(True)
 
    if self.ui.local_channel_combo.currentText() == "":
      pass
    elif self.ui.local_channel_combo.currentText() == "JPOP":
      subprocess.call("/path/to/youtube_jpop_playlist.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "POPS":
      subprocess.call("/path/to/youtube_pops_playlist.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "JAZZ":
      subprocess.call("/path/to/icecastjazz.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "CLASSIC":
      subprocess.call("/path/to/icecastclassical.sh &", shell=True)
    elif self.ui.local_channel_combo.currentText() == "BLUES":
      subprocess.call("/path/to/icecastblues.sh &", shell=True)
    else:
      pass
 
    if not self.ui.local_channel_combo.currentText() == "":
      self.ui.local_channel_combo.setEnabled(False)
      self.ui.local_radiko_combo.setEnabled(False)
      self.ui.local_news_combo.setEnabled(False)
    else:
      self.ui.local_channel_combo.setEnabled(True)
      self.ui.local_radiko_combo.setEnabled(True)
      self.ui.local_news_combo.setEnabled(True)
 
  def btnLocalYouTubeSkip(self):
    print("\"スキップ\" button was clicked")
    subprocess.call("/path/to/skip_playlist.sh &", shell=True)
 
  def btnLocalStop(self):
    print("\"Stop Music\" button was clicked")
    self.ui.local_channel_combo.setEnabled(True)
    self.ui.local_radiko_combo.setEnabled(True)
    self.ui.local_news_combo.setEnabled(True)
    subprocess.call("/path/to/stop_radio.sh &", shell=True)
 
  def onRpiMusicActivated(self):
    print("\"raspi_combo\" was selected")
 
    if not (self.ui.rpi_channel_combo.currentText() == "JPOP" or self.ui.rpi_channel_combo.currentText() == "POPS"):
      self.ui.rpi_youtube_skip_btn.setEnabled(False)
    else:
      self.ui.rpi_youtube_skip_btn.setEnabled(True)
 
    if self.ui.rpi_channel_combo.currentText() == "":
      pass
    elif self.ui.rpi_channel_combo.currentText() == "JPOP":
      self.ui.rpi_youtube_skip_btn.setEnabled(True)
      subprocess.call("/path/to/youtube_jpop_playlist_raspi.sh &", shell=True)
    elif self.ui.rpi_channel_combo.currentText() == "POPS":
      self.ui.rpi_youtube_skip_btn.setEnabled(True)
      subprocess.call("/path/to/youtube_pops_playlist_raspi.sh &", shell=True)
    elif self.ui.rpi_channel_combo.currentText() == "JAZZ":
      subprocess.call("/path/to/icecastjazz_raspi.sh &", shell=True)
    elif self.ui.rpi_channel_combo.currentText() == "CLASSIC":
      subprocess.call("/path/to/icecastclassical_raspi.sh &", shell=True)
    elif self.ui.rpi_channel_combo.currentText() == "BLUES":
      subprocess.call("/path/to/icecastblues_raspi.sh &", shell=True)
    else:
      pass
 
    if not self.ui.rpi_channel_combo.currentText() == "":
      self.ui.rpi_channel_combo.setEnabled(False)
      self.ui.rpi_radiko_combo.setEnabled(False)
      self.ui.rpi_news_combo.setEnabled(False)
    else:
      self.ui.rpi_channel_combo.setEnabled(True)
      self.ui.rpi_radiko_combo.setEnabled(True)
      self.ui.rpi_news_combo.setEnabled(True)
 
  def btnRpiYouTubeSkip(self):
    print("\"Raspi スキップ\" button was clicked")
    subprocess.call("/path/to/skip_playlist_raspi.sh &", shell=True)
 
  def btnRpiStop(self):
    print("\"Raspi Stop Music\" button was clicked")
    self.ui.rpi_channel_combo.setEnabled(True)
    self.ui.rpi_radiko_combo.setEnabled(True)
    self.ui.rpi_news_combo.setEnabled(True)
    subprocess.call("/path/to/stop_radio_raspi.sh &", shell=True)
 
  def onLocalRadikoActivated(self):
    print("\"local_combo\" was selected")
 
    if self.ui.local_radiko_combo.currentText() == "":
      pass
    elif self.ui.local_radiko_combo.currentText() == "JWave":
      subprocess.call("/path/to/radiko.sh -p FMJ &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "InterFM897":
      subprocess.call("/path/to/radiko.sh -p INT &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "TokyoFM":
      subprocess.call("/path/to/radiko.sh -p FMT &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "bayfm78":
      subprocess.call("/path/to/radiko.sh -p BAYFM78 &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "NACK5":
      subprocess.call("/path/to/radiko.sh -p NACK5 &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "FMヨコハマ":
      subprocess.call("/path/to/radiko.sh -p YFM &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "TBSラジオ":
      subprocess.call("/path/to/radiko.sh -p TBS &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "ニッポン放送":
      subprocess.call("/path/to/radiko.sh -p LFR &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "ラジオ日本":
      subprocess.call("/path/to/radiko.sh -p JORF &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "文化放送":
      subprocess.call("/path/to/radiko.sh -p QRR &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "ラジオNIKKEI第1":
      subprocess.call("/path/to/radiko.sh -p RN1 &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "ラジオNIKKEI第2":
      subprocess.call("/path/to/radiko.sh -p RN2 &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "放送大学":
      subprocess.call("/path/to/radiko.sh -p HOUSOU-DAIGAKU &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "東京NHK第1":
      subprocess.call("/path/to/radiko.sh -p JOAK &", shell=True)
    elif self.ui.local_radiko_combo.currentText() == "東京NHKFM":
      subprocess.call("/path/to/radiko.sh -p JOAK-FM &", shell=True)
    else:
      pass
 
    if not self.ui.local_radiko_combo.currentText() == "":
      self.ui.local_channel_combo.setEnabled(False)
      self.ui.local_radiko_combo.setEnabled(False)
      self.ui.local_news_combo.setEnabled(False)
    else:
      self.ui.local_channel_combo.setEnabled(True)
      self.ui.local_radiko_combo.setEnabled(True)
      self.ui.local_news_combo.setEnabled(True)
 
  def onRpiRadikoActivated(self):
    print("\"raspi_radiko_combo\" was selected")
 
    if self.ui.rpi_radiko_combo.currentText() == "":
      pass
    elif self.ui.rpi_radiko_combo.currentText() == "JWave":
      subprocess.call("/path/to/radiko_raspi.sh FMJ &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "InterFM897":
      subprocess.call("/path/to/radiko_raspi.sh INT &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "TokyoFM":
      subprocess.call("/path/to/radiko_raspi.sh FMT &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "bayfm78":
      subprocess.call("/path/to/radiko_raspi.sh BAYFM78 &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "NACK5":
      subprocess.call("/path/to/radiko_raspi.sh NACK5 &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "FMヨコハマ":
      subprocess.call("/path/to/radiko_raspi.sh YFM &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "TBSラジオ":
      subprocess.call("/path/to/radiko_raspi.sh TBS &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "ニッポン放送":
      subprocess.call("/path/to/radiko_raspi.sh LFR &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "ラジオ日本":
      subprocess.call("/path/to/radiko_raspi.sh JORF &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "文化放送":
      subprocess.call("/path/to/radiko_raspi.sh QRR &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "ラジオNIKKEI第1":
      subprocess.call("/path/to/radiko_raspi.sh RN1 &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "ラジオNIKKEI第2":
      subprocess.call("/path/to/radiko_raspi.sh RN2 &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "放送大学":
      subprocess.call("/path/to/radiko_raspi.sh HOUSOU-DAIGAKU &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "東京NHK第1":
      subprocess.call("/path/to/radiko_raspi.sh JOAK &", shell=True)
    elif self.ui.rpi_radiko_combo.currentText() == "東京NHKFM":
      subprocess.call("/path/to/radiko_raspi.sh JOAK-FM &", shell=True)
    else:
      pass
 
    if not self.ui.rpi_radiko_combo.currentText() == "":
      self.ui.rpi_channel_combo.setEnabled(False)
      self.ui.rpi_radiko_combo.setEnabled(False)
      self.ui.rpi_news_combo.setEnabled(False)
    else:
      self.ui.rpi_channel_combo.setEnabled(True)
      self.ui.rpi_radiko_combo.setEnabled(True)
      self.ui.rpi_news_combo.setEnabled(True)
 
  def onLocalNewsActivated(self):
    print("\"local_news_combo\" was selected")
 
    if self.ui.local_news_combo.currentText() == "":
      pass
    elif self.ui.local_news_combo.currentText() == "Yahoo! Topics":
      subprocess.call("/path/to/get_yahoo_news.py &", shell=True)
    elif self.ui.local_news_combo.currentText() == "BBC World News":
      subprocess.call("mplayer http://bbcwssc.ic.llnwd.net/stream/bbcwssc_mp1_ws-eieuk &", shell=True)
    elif self.ui.local_news_combo.currentText() == "ABC News":
      subprocess.call("mplayer -playlist http://abc.net.au/res/streaming/audio/aac/news_radio.pls &", shell=True)
 
    if not self.ui.local_news_combo.currentText() == "":
      self.ui.local_channel_combo.setEnabled(False)
      self.ui.local_radiko_combo.setEnabled(False)
      self.ui.local_news_combo.setEnabled(False)
    else:
      self.ui.local_channel_combo.setEnabled(True)
      self.ui.local_radiko_combo.setEnabled(True)
      self.ui.local_news_combo.setEnabled(True)
 
  def onRpiNewsActivated(self):
    print("\"raspi_news_combo\" was selected")
 
    if self.ui.rpi_news_combo.currentText() == "":
      pass
    elif self.ui.rpi_news_combo.currentText() == "Yahoo! Topics":
      subprocess.call("/path/to/get_yahoo_news_raspi.sh &", shell=True)
    elif self.ui.rpi_news_combo.currentText() == "BBC World News":
      subprocess.call("/path/to/bbc_news_radio_raspi.sh &", shell=True)
    elif self.ui.rpi_news_combo.currentText() == "ABC News":
      subprocess.call("/path/to/bbc_news_radio_raspi.sh &", shell=True)
 
    if not self.ui.rpi_news_combo.currentText() == "":
      self.ui.rpi_channel_combo.setEnabled(False)
      self.ui.rpi_radiko_combo.setEnabled(False)
      self.ui.rpi_news_combo.setEnabled(False)
    else:
      self.ui.rpi_channel_combo.setEnabled(True)
      self.ui.rpi_radiko_combo.setEnabled(True)
      self.ui.rpi_news_combo.setEnabled(True)
 
  def onLocalWeatherActivated(self):
    print("\"local_weather_combo\" was selected")
 
    if self.ui.local_weather_combo.currentText() == "":
      pass
    elif self.ui.local_weather_combo.currentText() == "今日の天気":
      subprocess.call("/path/to/today_weather.py &", shell=True)
    elif self.ui.local_weather_combo.currentText() == "今日の気温":
      subprocess.call("/path/to/today_temperature.py &", shell=True)
    elif self.ui.local_weather_combo.currentText() == "明日の天気":
      subprocess.call("/path/to/tomorrow_weather.py &", shell=True)
    elif self.ui.local_weather_combo.currentText() == "明日の気温":
      subprocess.call("/path/to/tomorrow_temperature.py &", shell=True)
    elif self.ui.local_weather_combo.currentText() == "明後日の天気":
      subprocess.call("/path/to/day_after_tomorrow_weather.py &", shell=True)
 
  def onRpiWeatherActivated(self):
    print("\"raspi_weather_combo\" was selected")
 
    if self.ui.rpi_weather_combo.currentText() == "":
      pass
    elif self.ui.rpi_weather_combo.currentText() == "今日の天気":
      subprocess.call("/path/to/get_yahoo_weather_raspi.sh &", shell=True)
    elif self.ui.rpi_weather_combo.currentText() == "今日の気温":
      subprocess.call("/path/to/bbc_weather_radio_raspi.sh &", shell=True)
    elif self.ui.rpi_weather_combo.currentText() == "明日の天気":
      subprocess.call("/path/to/bbc_weather_radio_raspi.sh &", shell=True)
    elif self.ui.rpi_weather_combo.currentText() == "明日の気温":
      subprocess.call("/path/to/bbc_weather_radio_raspi.sh &", shell=True)
    elif self.ui.rpi_weather_combo.currentText() == "明後日の天気":
      subprocess.call("/path/to/bbc_weather_radio_raspi.sh &", shell=True)
 
  def btnMainMenu(self):
    print("\"メインメニュー\" button was clicked")
    subprocess.call("/path/to/home_elec_panel.sh MAIN &", shell=True)
 
  def btnAircon(self):
    print("\"エアコン\" button was clicked")
    subprocess.call("/path/to/home_elec_panel.sh AIRCON1 &", shell=True)
 
  def btnTV(self):
    print("\"テレビ\" button was clicked")
    subprocess.call("/path/to/home_elec_panel.sh TV1 &", shell=True)
 
def main():
  app = QApplication(sys.argv)
  form = ExampleApp()
  form.show()
  app.exec_()
 
if __name__ == '__main__':
  main()
$ /path/to/sp_qt_ctrl_panel_alt.py
$

 音楽タブは、任意に選んだYouTubeプレイリストとICECASTのストリーム局、手持ちの古いCDをリッピングしてラズパイサーバに上げてジャンル分けしてあるプレイリスト。

 Radikoタブは、NHK第一とNHK FM含む各種放送局。

 ニュースタブは、Yahoo!ピックアップ(主要ニュース?)のOpen JTalkによる読み上げとABC NEWS、BBC WorldNewsのストリーム配信。

 天気タブは、livedoor天気情報をWebスクレイピングで取得、Open JTalkで読み上げすることにしています。

自作スマートスピーカー用GUI操作パネルからブラウザ版家電操作パネルを起動

 家電タブには、ボタン(Push Button)のみを配置しました。

 WiFi搭載マイコンESP8266/WiFi・Bluetooth搭載マイコンESP32は、常時電源ONさせているわけではありません。

 そこで疎通確認の上、ESPモジュールにアップロードした操作パネル用HTMLファイル(mDNSにより例えばトップページはdomain.local)にアクセスさせることに。

 より具体的には、[firefox -private espctrl_panel.local]などとしたshellスクリプトを書き、.uiファイルからこれを呼ぶことでFirefoxブラウザ版スマートホーム操作パネルを表示するようにしてみました。

 スマートスピーカーとは離れますが、同様に天気タブにボタンを追加して天気予報のサイトを表示、また、交通タブでも作って電車の運行状況や車の渋滞情報のサイトを表示するボタンを配置するのもよいかもしれないですね。

 GUI操作でブラウザを表示するなら、SNSやGMail、クラウドなんかに遷移する用のタブやボタンを作ってみるのもありかもしれませんね。

 アラームタブに変更するかもしれないタイマータブには、5分刻みで5〜60分までを選択可能なコンボとタイマー解除ボタンを配置しました。

 ちなみにJulius辞書をあまり膨らませたくないこともあり、音声操作では5〜30分まで指定できるようにしていますが、スクリプト自体は引数1つで分指定、2つで時分指定など汎用的に作ってあるのでGUIパネル上からなら、5分刻みにする必要すらなく、選択肢は、いくらでも追加可能です。

 未実装の内、追加予定の伝言タブや音声メモタブは、録音再生取り消しボタンを配置するかとか、共にアラーム設定内容や伝言メッセージの内容を表示させるか否かは思案中です。

 日付時刻については、操作パネルを使うくらいなら読み上げはしないまでもPCのアプリとして既に立派なものがありますし、旧暦干支については、必要な気がしない...よく考えると伝言音声メモも含め、GUIパネルには実装しない可能性大...。

 これら、各機能の詳細については、冒頭や後段のリンクにあります。

ウェブ造ホーム前へ次へ