この作品は、ラズベリーパイでの作業を楽しくする機能を追加した持ち運び可能な取っ手付きの 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スマホ/タブレットを操作させる
-
tktk360
さんが
2020/12/30
に
編集
をしました。
(メッセージ: 初版)
-
tktk360
さんが
2020/12/30
に
編集
をしました。
-
tktk360
さんが
2020/12/30
に
編集
をしました。
-
tktk360
さんが
2020/12/30
に
編集
をしました。
ログインしてコメントを投稿する