tktk360のアイコン画像
tktk360 2020年12月30日作成 (2020年12月30日更新)
製作品 製作品 閲覧数 1717
tktk360 2020年12月30日作成 (2020年12月30日更新) 製作品 製作品 閲覧数 1717

作業快適 ラズパイながらYoutube操作

作業快適 ラズパイながらYoutube操作

この作品は、ラズベリーパイでの作業を楽しくする機能を追加した持ち運び可能な取っ手付きの PC です。
ラズパイの入ったケースに手をかざして、スワイプなどのジェスチャーやタップ操作、音声認識で、Amazon Fire TV Stick を操作できます。
ラズパイながらYoutubeの動画

作成の動機

  • PCで物書きやプログラミングをしている時、YouTubeで、音楽を流したり、合間に動画を見たりしませんか?
    パソコンのブラウザで再生してもいいですが、ラズパイだとスペック的にちょっとしんどいです。
    あと、マウスでカチカチ操作するのも面倒だと思ってます。そこで、TV に Amazon Fire TV Stick をつけて、再生しているのですが、この操作をするときに、よくリモコンが行方不明になります。何故でしょう。。。
    そこで、ラズパイのケースに手をかざして、ジェスチャやタップで、Amazon Fire TV Stick を操作させることにしています。

ジェスチャー操作について

  • Flick!」というジェスチャセンサ搭載の白い板をケースに埋め込んでいます。その板の上で、スワイプや、タップの動作をプログミング言語で判断してわかるんです。プログラミング言語は、「Python」を使っています。

Fire TV Stick の操作について

  • Android Debug Bridge(通称:adb)を使って、ラズパイから、同一ネットワークに繋いでいる Fire TV Stick に命令を送っています。さらに、音声認識にも対応していて、スマートスピーカのように、声で操作できるようにしています。
    声の操作に利用している音声認識は、REST API の DoCoMo の音声認識 API を使っています。
    個人だと無料で試せるのが良いです。白い板の手前をタップすることで、音声認識開始のトリガーとしています。

パーツ

  • 作成に使用したパーツは下記となります。
品目
Raspberry Pi 3
micro sd card 32GB(Raspberyy Pi OS用)
NeoPixel (5LED)
Flick! Large
USB接続の小型マイク
取っ手付きプラケース
リフォーム用木目シール(プラケースの装飾用)
Amazon Fire TV Stick
モバイルバッテリー(RaspberryPi駆動用)
ジャンパー線
両面テープ(機具固定用)

システム構成図
システム構成図

筐体の作成

  • 下記を参考に筐体を作成してください。
    1.取っ手付きプラケースに、NEOPIXEL表示用とFlick!Large固定用の穴をあけテープで固定します。
    2.配線図を元に、ラズパイに、Flick! Large、NeoPixelを接続します。
    3.モバイルバッテリー、ライン出力でのスピーカー、USB接続でのマイク(音声認識使用の場合)を接続します。
    4.ケース内にラズパイ毎、両面テープで固定します。
    その他:ケースを自由にデコレーションしてください。私はリフォーム用木目シートを張り付けてみました。
    映像表示は、好みに合わせて、HDMI出力またはリモートデスクトップで接続してください。配線は、下図を参考ください。

配線図

Amazon Fire TV Stick の準備

  • ADBを使用してFire TVに接続する方法を参考に、Amazon Fire TVでデバッグを有効にしてください。

  • IPアドレスの確認方法
    「設定」の「My Fire TV」を選択してください。
    「バージョン情報」を選択してください。
    「ネットワーク」を選択すると、右側にIPアドレスが表示されます。

Amazon Fire TV Stick の操作
ADBコマンドの対応は、下記となります。
この内容をラズパイのプログラムで操作させる訳です。

  • 接続
    adb connect ip
  • 方向キーパッドの上下左右
    adb shell input keyevent 19 (上)
    adb shell input keyevent 20 (下)
    adb shell input keyevent 21 (左)
    adb shell input keyevent 22 (右)
  • 選択
    adb shell input keyevent 66
  • 戻る
    adb shell input keyevent 4
  • ホーム
    adb shell input keyevent 3
  • メニュー
    adb shell input keyevent 1
  • 再生/一時停止
    adb shell input keyevent 85
  • 早戻し
    adb shell input keyevent 88
  • 早送り
    adb shell input keyevent 87

音声認識(オプション)
音声認識を利用する場合は、事前にAPIキーを申請し、取得してください。
APIキーの取得:docomo音声認識API
音声認識は、個人差があるため、いくつか試してみて認識結果が良かったキーワードをプログラムに採用しています。

  • Enter
    '決定' or '選択' or '見'
    #Enter

  • Play-Pause
    '再生' or '止' or 'プレ' or 'ポーズ'

  • Return
    '戻' or 'キャンセル'

  • Home
    'ホーム'

使用ライブラリ

  • Flick
    Flick Github

  • NeoPixel
    sudo pip3 install rpi_ws281x adafruit-circuitpython-neopixel
    sudo python3 -m pip install --force-reinstall adafruit-blinka

プログラムの起動方法
何はともあれ、プログラムを動作させて見ましょう。
Githubからソースコードを取得してください。

起動スクリプトは、 「sudo python flick-tube .py amazon firetv stickのipaddress」になります。

プログラムの説明

  • Pythonのバージョン違いの組み合わせで動作するアプリケーションとなっています。
    ・アプリケーション本体(Flick含む)は、python2系で動作しています。
    ・NEOPIXEL は、python3系で動作しています。

python2系

  • アプリケーション本体:flick-tube .py
    ・ジェスチャーFlick Largeのコントロール結果に応じて、adbfunc.pyのAPIの呼び出し

  • Amazon Fire TV Stick の操作:adbfunc .py
    ・ADBコマンドで、Amazon Fire TV Stick の操作
    ・効果音の再生
    ・NEOPIXELの効果表示呼び出し
    ・音声認識と認識結果に応じた操作(取得したDoCoMo の音声認識APIキーを記入)

python3系(別プロセスアプリケーション)

  • NEOPIXELの効果表示:callpixel .py, pixelfunc .py
    ・NEOPIXELの効果表示
    ・起動コマンド
      sudo python3 callpixel .py 数値
     「数値」
     0:レインボー
     1:右から左
     2:左から右
     3:左右から中央
     4:中央から左右
     5:クリアー
     6:点灯
     
    以下にソースコードを掲載します
    効果音ファイルは、Githubから取得してください

アプリケーション本体(flick-tube.py)

#!/usr/bin/env python import signal import flicklib import time import curses import sys import adbfunc from curses import wrapper some_value = 5000 @flicklib.move() def move(x, y, z): global xyztxt xyztxt = '{:5.3f} {:5.3f} {:5.3f}'.format(x,y,z) @flicklib.flick() def flick(start,finish): global flicktxt global isClearWin flicktxt = start + ' - ' + finish if finish == "north" : adbObj.upFunction() elif finish == "south" : adbObj.downFunction() elif finish == "east" : adbObj.rightFunction() elif finish == "west" : adbObj.leftFunction() isClearWin = True @flicklib.airwheel() def spinny(delta): global some_value global airwheeltxt some_value += delta if some_value < 0: some_value = 0 if some_value > 10000: some_value = 10000 airwheeltxt = str(some_value/100) @flicklib.double_tap() def doubletap(position): global doubletaptxt doubletaptxt = position @flicklib.tap() def tap(position): global taptxt taptxt = position if position == "center" : adbObj.selectFunction() elif position == "north" : adbObj.homeFunction() elif position == "south" : #adbObj.menuFunction() adbObj.voiceFunction() elif position == "east" : adbObj.playPauseFunction() elif position == "west" : adbObj.returnFunction() isClearWin = True @flicklib.touch() def touch(position): global touchtxt global isClearWin touchtxt = position # # Main display using curses # def main(stdscr): global xyztxt global flicktxt global airwheeltxt global touchtxt global taptxt global doubletaptxt global isClearWin isClearWin = True xyztxt = '' flicktxt = '' flickcount = 0 airwheeltxt = '' airwheelcount = 0 touchtxt = '' touchcount = 0 taptxt = '' tapcount = 0 doubletaptxt = '' doubletapcount = 0 fw_info = flicklib.getfwinfo() datawin = curses.newwin( 8, curses.COLS - 6, 2, 3) fwwin = curses.newwin(10, curses.COLS - 6, 11, 3) # Update data window continuously until Control-C while True: # Fill firmware info window. if isClearWin : isClearWin = False # Clear screen and hide cursor stdscr.clear() curses.curs_set(0) # Add title and footer exittxt = 'Control-C to exit' title = '**** Flick Demo ****' stdscr.addstr( 0, (curses.COLS - len(title)) / 2, title) stdscr.addstr(22, (curses.COLS - len(exittxt)) / 2, exittxt) stdscr.refresh() fwwin.erase() fwwin.border() fwwin.addstr(1, 2, 'Firmware valid: ' + 'Yes' if fw_info['FwValid'] == 0xaa else 'No') fwwin.addstr(2, 2, 'Hardware Revison: ' + str(fw_info['HwRev'][0]) + '.' + str(fw_info['HwRev'][1])) fwwin.addstr(3, 2, 'Params Start Addr: ' + '0x{:04x}'.format(fw_info['ParamStartAddr'])) fwwin.addstr(4, 2, 'Library Loader Version: ' + str(fw_info['LibLoaderVer'][0]) + '.' + str(fw_info['LibLoaderVer'][1])) fwwin.addstr(5, 2, 'Library Loader Platform: ' + 'Hillstar' if fw_info['LibLoaderPlatform'] == 21 else 'Woodstar') fwwin.addstr(6, 2, 'Firmware Start Addr: 0x' + '{:04x}'.format(fw_info['FwStartAddr'])) fwver_part1, fwver_part2 = fw_info['FwVersion'].split(';DSP:') fwwin.addstr(7, 2, 'Firmware Version: ' + fwver_part1) fwwin.addstr(8, 2, 'DSP: ' + fwver_part2) fwwin.refresh() datawin.erase() datawin.border() datawin.addstr(1, 2, 'X Y Z : ' + xyztxt) datawin.addstr(2, 2, 'Flick : ' + flicktxt) datawin.addstr(3, 2, 'Airwheel : ' + airwheeltxt) datawin.addstr(4, 2, 'Touch : ' + touchtxt) datawin.addstr(5, 2, 'Tap : ' + taptxt) datawin.addstr(6, 2, 'Doubletap : ' + doubletaptxt) datawin.refresh() xyztxt = '' if len(flicktxt) > 0 and flickcount < 5: flickcount += 1 else: flicktxt = '' flickcount = 0 if len(airwheeltxt) > 0 and airwheelcount < 5: airwheelcount += 1 else: airwheeltxt = '' airwheelcount = 0 if len(touchtxt) > 0 and touchcount < 5: touchcount += 1 else: touchtxt = '' touchcount = 0 if len(taptxt) > 0 and tapcount < 5: tapcount += 1 else: taptxt = '' tapcount = 0 if len(doubletaptxt) > 0 and doubletapcount < 5: doubletapcount += 1 else: doubletaptxt = '' doubletapcount = 0 time.sleep(0.1) args = sys.argv n = len(args) adbObj = adbfunc.ADBFuncClass() if n > 1: ip = args[1] adbObj.setIpAddress(ip) wrapper(main)

アプリケーション本体(adbfunc.py)

# -*- coding: utf-8 -*- import os import sys import termios import time import subprocess from subprocess import call import requests class ADBFuncClass: _isAdb = False _isVoice = False CARD = 0 DEVICE = 0 RECDEV = 0 RECCD = 1 rec_time = 2 rec_path = 'temp.wav' DOCOMO_API = 'XXXXXXXXXXXXXXXXXXXXX' URL_RECOG = 'https://api.apigw.smt.docomo.ne.jp/amiVoice/v1/recognize?APIKEY=' def __init__(self): self.pixelEffect(5) time.sleep(0.25) def playSound(self, file): print(file) os.system('aplay -D plughw:{},{} {}'.format(self.CARD, self.DEVICE, file)) def pixelEffect(self, cmd): print(cmd) subprocess.call("sudo python3 callpixel.py " + str(cmd), shell=True) def leftFunction(self): print("Left") if self._isAdb: call(["adb","shell","input","keyevent","21"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("swipe.wav") self.pixelEffect(1) #self.swipeSound.play() #time.sleep(1.25) def rightFunction(self): print("Right") if self._isAdb: call(["adb","shell","input","keyevent","22"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("swipe.wav") self.pixelEffect(2) #self.swipeSound.play() #time.sleep(1.25) def upFunction(self): print("Up") if self._isAdb: call(["adb","shell","input","keyevent","19"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("swipe.wav") self.pixelEffect(3) #self.swipeSound.play() #time.sleep(1.25) def downFunction(self): print("Down") if self._isAdb: call(["adb","shell","input","keyevent","20"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("swipe.wav") self.pixelEffect(4) #self.swipeSound.play() #time.sleep(1.25) def selectFunction(self): print("Select") if self._isAdb: call(["adb","shell","input","keyevent","KEYCODE_DPAD_CENTER"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("enter.wav") self.pixelEffect(0) #self.enterSound.play() #time.sleep(1.25) def menuFunction(self): print("Menu") if self._isAdb: call(["adb","shell","input","keyevent","1"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("enter.wav") self.pixelEffect(6) #self.enterSound.play() #time.sleep(1.25) def playPauseFunction(self): print("Play / Pause") if self._isAdb: call(["adb","shell","input","keyevent","85"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("playpause.wav") self.pixelEffect(6) #self.playPauseSound.play() #time.sleep(1.25) def homeFunction(self): print("Home") if self._isAdb: call(["adb","shell","input","keyevent","3"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("enter.wav") self.pixelEffect(6) #self.enterSound.play() #time.sleep(1.25) def returnFunction(self): print("Return") if self._isAdb: call(["adb","shell","input","keyevent","4"]) if self._isVoice: self.playSound("vkashi.wav") else: self.playSound("return.wav") self.pixelEffect(6) #self.returnSound.play() #time.sleep(1.25) def mainLoop(self): fd = sys.stdin.fileno() old = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] &= ~termios.ICANON new[3] &= ~termios.ECHO try: while True: #KeyEvent termios.tcsetattr(fd, termios.TCSANOW, new) ch = sys.stdin.read(1) #print(ch) if ch == '4': self.leftFunction() elif ch == '6': self.rightFunction() elif ch == '8': self.upFunction() elif ch == '2': self.downFunction() elif ch == '5': self.selectFunction() elif ch == '9': self.menuFunction() elif ch == '3': self.playPauseFunction() elif ch == '1': self.homeFunction() elif ch == '7': self.returnFunction() elif ch == 's': self.voiceFunction() finally: termios.tcsetattr(fd, termios.TCSANOW, old) def setIpAddress(self,ip): self._isAdb = True print(ip) print("ADB ON") call(["adb","kill-server"]) call(["adb","start-server"]) call(["adb","connect", ip]) def voiceFunction(self): print("Voice") self.pixelEffect(0) self.playSound("vgoyou.wav") a1 = 'sudo arecord -r 16000 -f S16_LE -d ' + str(self.rec_time) + ' -D plughw:{},{} '.format(self.RECCD, self.RECDEV) + self.rec_path subprocess.call(a1, shell=True) url = self.URL_RECOG + self.DOCOMO_API files = {"a": open(self.rec_path, 'rb'), "v": "on"} #files = {"a": open('vkashi.wav', 'rb'), "v": "on"} r = requests.post(url, files=files) print(r.text) rec = r.json()['text'] txt = rec.encode('utf-8') print(txt) self._isVoice = True #Enter if '決定' in txt or '選択' in txt or '見' in txt: self.selectFunction() #Play-Pause elif '再生' in txt or '止' in txt or 'プレ' in txt or 'ポーズ' in txt: self.playPauseFunction() #Return elif '戻' in txt or 'キャンセル' in txt: self.returnFunction() #Home elif 'ホーム' in txt: self.homeFunction() else: print("None") self.playSound("vnone.wav") self._isVoice = False

アプリケーション本体(callpixel.py)

import sys import time import pixelfunc if __name__ == "__main__": args = sys.argv n = len(args) if n > 1: cmd = int(args[1]) #print(cmd) pixelObj = pixelfunc.PixelFuncClass() if cmd == 0: pixelObj.rainbow_cycle(0.001) pixelObj.clear_pixel() elif cmd == 1: pixelObj.rightToLeft_cycle(0.05,0,255,0) time.sleep(0.6) pixelObj.clear_pixel() elif cmd == 2: #print("leftToRight_cycle") pixelObj.leftToRight_cycle(0.05,50,255,0) time.sleep(0.6) pixelObj.clear_pixel() elif cmd == 3: pixelObj.edgeToCenter_cycle(0.08,0,50,255) time.sleep(0.6) pixelObj.clear_pixel() elif cmd == 4: pixelObj.centerToEdge_cycle(0.08,0,150,255) time.sleep(0.6) pixelObj.clear_pixel() elif cmd == 5: pixelObj.clear_pixel() elif cmd == 6: pixelObj.fill_cycle(0.08,0,0,255) time.sleep(0.6) pixelObj.clear_pixel()

アプリケーション本体(pixelfunc.py)

import time import board import neopixel class PixelFuncClass: pixel_pin = board.D21 # The number of NeoPixels num_pixels = 8 # The order of the pixel colors - RGB or GRB. Some NeoPixels have red and green reversed! # For RGBW NeoPixels, simply change the ORDER to RGBW or GRBW. ORDER = neopixel.GRB def __init__(self): self.pixels = neopixel.NeoPixel(self.pixel_pin, self.num_pixels, brightness=0.2, auto_write=False, pixel_order=self.ORDER) def wheel(self,pos): # Input a value 0 to 255 to get a color value. # The colours are a transition r - g - b - back to r. if pos < 0 or pos > 255: r = g = b = 0 elif pos < 85: r = int(pos * 3) g = int(255 - pos*3) b = 0 elif pos < 170: pos -= 85 r = int(255 - pos*3) g = 0 b = int(pos*3) else: pos -= 170 r = 0 g = int(pos*3) b = int(255 - pos*3) return (r, g, b) if self.ORDER == neopixel.RGB or self.ORDER == neopixel.GRB else (r, g, b, 0) def rainbow_cycle(self,wait): for j in range(255): for i in range(self.num_pixels): pixel_index = (i * 256 // self.num_pixels) + j self.pixels[i] = self.wheel(pixel_index & 255) self.pixels.show() time.sleep(wait) def clear_pixel(self): self.pixels.fill((0, 0, 0)) # Uncomment this line if you have RGBW/GRBW NeoPixels # pixels.fill((255, 0, 0, 0)) self.pixels.show() def rightToLeft_cycle(self,wait,r,g,b): self.clear_pixel() for i in range(self.num_pixels): self.pixels[i] = (r, g, b) self.pixels.show() time.sleep(wait) def leftToRight_cycle(self,wait,r,g,b): self.clear_pixel() len = self.num_pixels for i in range(len): self.pixels[len - 1 - i] = (r, g, b) self.pixels.show() time.sleep(wait) def edgeToCenter_cycle(self,wait,r,g,b): self.clear_pixel() len = self.num_pixels halfLen = int(len / 2) for i in range(halfLen): self.pixels[i] = (r, g, b) self.pixels[len - 1 - i] = (r, g, b) self.pixels.show() time.sleep(wait) def centerToEdge_cycle(self,wait,r,g,b): self.clear_pixel() len = self.num_pixels halfLen = int(len / 2) for i in range(halfLen): self.pixels[halfLen - 2 - i] = (r, g, b) self.pixels[halfLen - 1 + i] = (r, g, b) self.pixels.show() time.sleep(wait) def fill_cycle(self,wait,r,g,b): self.clear_pixel() self.pixels.fill((r,g,b)) self.pixels.show() time.sleep(wait)

最後に
使用風景

  • この内容をもとに、ちょっとした改造でオリジナリティを出してみてはいかがでしょうか。
    例えば、以下のものが考えられます。是非チャレンジしてみてください。

【応用編】
・ジェスチャー操作内容を増やす
 ・指をまわすことで、音量を調整させる
・Amazon Fire TV Stick 以外の機器操作
 ・ADBコマンドでAndroidスマホ/タブレットを操作させる

1
ログインしてコメントを投稿する