娘の誕生日プレゼントを作る 4歳 テック話
先日投稿した次女の4歳のプレゼントについて、テクニカルな話を紹介してみようと思います。どういう気持でつくったかなどは、こちらに書いてあります。
さっそく作る目線で語っていきます。
作る前に下調べとスタディ
今回のプレゼントでは、以下のようなことが出来ます。
・ボタンを押すと自撮り的に写真が撮れる
・とった写真はすぐにSlackの特定のチャンネルに投稿される
・とった写真はデバイスの画面に表示される
・Slack上で返信的に画像をアップロードするとそれがデバイスに表示される
写真でコミュニケーションをとることがテーマになるので、相互にやりとりできなければいけません。これを実現するには、Slackで実装するならばBotとしてアプリを実装することになります。Slackについてはよく仕事でも使っているので、多少知識はありますが、今回のはデバイスが随時Slackの状態を監視して、その状況をみながら対応するような動きが必要になります。なんとなくそういう作り方ができるということは知っていたので、RaspberryPiでPythonで実装するという条件のもとリサーチをしました。
python slack bot
このキーワードで検索すると、だいたいslackbotというライブラリの情報が出てきます。
で、まずはこれを試そうと色んな人の解説を見ながら進めていきますが、どうもうまくいきません。Slack上でAppを作成するために登録をするんですが、解説の記事とは画面が違うし、scopeとか昔なかった項目があります。なにか大きな改変があったようです。困りますね。
もう少し丁寧に見ていくと、slackbotはReal Time Messaging APIというのを使ってるみたいですが、最新のAPIでは、このReal Time Messaging APIが使えないようです。レガシーBotとかいう登録をすると、一応今でも使えるみたいですが、将来的に使えなくなる予感もあるし、仕事でBot開発を自分でやってみたい気もするので、ここは今の書き方を調べておくことにします。
今は、Socket Modeというのがあるようです。そしてそれを使うのに、Slackが公式で配布してるっぽいBoltというライブラリのPython版を使うことで実装できるっぽいことがわかりました。公式ページにも解説があります。
これを軽くMac上のPythonでテストをしてみたところ、無事にBotっぽい何かが作れました。すごく良いです。RaspberryPi単独でSlack上でBotにメンション飛ばすとそれに答えるカタチで投稿したり、なにか処理を走らせたりが超簡単に書けます。過去に作った娘のプレゼントもこれを活用すると相互のやり取りが簡単に実現できてとても良さそうです。夢は膨らむ!
パーツ選定
今回はできるだけ小さく作りたいけど、処理は早いほうがなにかといいし、サクサク開発するには、RaspberryPi Zeroシリーズは、VSCodeのRemote Developmentが使えないので、RaspberryPi 3A+にしました。
RaspberryPi 3Bとかと比べて、EthernetのポートがなかったりUSBのポートが少なかったりしますが、その分薄くて小さくて組み込みには良さそうです。CPUは3B+と同じなので、違和感なく開発できそうです。
ディスプレイを何にするかという問題がありました。今回は写真をとって投稿ということで、Instagram的に正方形とかだといいなーと漠然と思います。デバイス的にもかわいい見た目が期待できます。
raspberrypi square display
希望通りの単語で検索すると出ました。
タッチ機能付きの正方形のディスプレイがありました。解像度も720 x 720と十分ですし、サイズも丁度良さそう。しかもよく見るとタッチがないバージョンもありました。そのほうがベゼルも小さくてよさそうなので、そっちにしました。
あと必要なのは、カメラとスイッチですね。
カメラは一応きれいに撮れそうなHigh Qualityというシリーズが出てるので一応みてみました。
めっちゃきれいに撮れるけどさ、デカイよ。。広角レンズもあるしすごく良かったけど、デカイよ。。。一眼カメラ風のカメラをRaspberryPiで作るなら間違いなくこれですが、今回は違うかな。。ということで、これは却下。
で、定番のカメラモジュールを改めてみてみます。
薄くてとても良さそうですが、うーんちょっと基板が大きい。これを内蔵しようとすると、外装のベゼルみたいなところが太くなっちゃう。もっと小さいのがないか調べました。
ありました。Spy Camera。これなら小さいので良さそうです。画質は不安ですが、買ってみます。
スイッチは、前からプレゼントシリーズで使っているタクトスイッチの大きなボタンパーツ付きのこれを使います。
パーツが選定できたので、次は設計です。
CADでパーツを組み立てる
Fusion 360で設計します。まずは全てのパーツを立体化しないといけません。今は集合知の時代なので、まずは検索します。誰かが立体化してる可能性があります。
でも結局、RaspberryPi 3A+しかモデルデータはありませんでした。。。スイッチは前も使ってるので自分で立体化したデータがあるので、それでいいとしても、カメラとディスプレイ部がデータがない。。なのでこれはパーツの到着を待ってノギスで図りながら立体化します。(図面みて起こしてもいいんですが、現物が一番確実なので)
シンプルです。このぐらいのパーツ点数で行けるんだからいい時代です。
愚直にレイアウトしたらこうなりました。
なんかスイッチのところが広いですね。
ディスプレイのベゼルが下側だけ広いんです。これのせいでなんとなくこうなっちゃいます。でも、モニターなんて回転して作ってもどうせ設定で回せるでしょってことで、軽く調べると、HyperPixelの設定ファイルに回転の設定があったので、問題なさそうです。ということで、設計を直しました。(左右には余裕があるので、幅広の部分を横に持っていくべく、90度回転)
あー、随分よくなりました。左右の幅と上下の幅は微妙に違うんですが、カメラの穴とか、スイッチがつくので、このぐらいの幅があるほうが、幅が揃って見えるということで調整しました。
中身はこういう雰囲気です。で、HyperPixelはRaspberryPiとスペーサーで連結するようにパーツがセットで届いてたので、それで固定することにしました。RaspberryPi 3A+なのでもうすこしスペースを攻められそうでしたが、まぁそこまで必死に薄くする必要もなさそうだし、スペースはきっと役に立つということで。
木をCNCで切削するので、表パーツと裏パーツで分割式にします。そうすることで、スイッチやカメラも挟み込む事ができます。
と、こんな具合で設計が完成しましたので、ひとまず切削します。今回はメープルを使います。
切削
Fusion360でCAMができますので、設定していきます。
削った結果が以下です。
メープルはいいですね。きれいに削れました。
組み立て
配線って言ってもちょろいです。カメラはRaspberryPiのカメラコネクタに繋ぐだけ。スイッチはGPIOとGNDにつなぐだけ(内部のPullupを使う)。あとは電源ですが、USBケーブルを刺すとスペース的にきつかったので、切って直接RaspberryPiにはんだ付けしちゃいました。GPIOでもいいんですが、一応USBコネクタの裏側に接続。(保護回路あるかもしれないし)
一通り配線ができたら、一旦蓋をしてみます。いい感じに組み上がりました。
一応いったんばらして、木の部分は蜜蝋を塗り込んで仕上げました。防水的な効果と汚れがつきにくいなどいろいろ良いことがあります。
コーディング
コーディングはわりと安心です。スタディをしてあったので、それらのモジュールを組み立てていきます。
これ説明すると長くなるのですが、一応かいつまんで。
ボタンの扱い
超定番です。RPi.GPIOを使います。配線が楽なので、pullupを使います。
import RPi.GPIO as GPIO
# setup gpio
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(ButtonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(ButtonPin, GPIO.FALLING, callback=callback, bouncetime=300)
抜粋すると上記のような感じです。add_event_detectでbounceTimeを指定することでチャタリングの誤動作防止ができます。ソフト的なチャタリング回避ですね。ボタンが押されたらcallbackが呼ばれるというとても便利な機能。
カメラ撮影シーン
こちらも定番のPiCameraというのがあります。Pythonのライブラリになっています。カメラの様々な設定もできるので、とてもいいです。カメラも実は回転した向きで実装してるので、回転必要ですが、このライブラリで簡単に回せます。
撮影前のプレビューの機能もあるし、水平方向フリップもできるので、iPhoneのインカメラみたいな鏡写し状態にできます。
しかもoverlay image機能があって、プレビューの画面の上に画像を重ねる機能があります。これを使ってカウントダウンの文字を出します。ついでに、ベゼルが角丸なので、画面も角丸にマスクしました。
そしてこのときにわかったのが、previewを止めてもこのoverlay機能で画像を画面に出せちゃいました。その上、overlayはマルチレイヤー対応なので、画面の角丸マスクは常に出しといて、その下の画像を切り替えるという使い方ができました。なので、PiCameraで撮影済みの写真を出すのもやっちゃいます。PiGameとか使おうかと思ってましたが、手間が省けました。
Slack連携
Slack系の機能は前述のBoltとSlackAPI(Boltを入れると自動で入る)、あと写真のアップロードにはSlackerを使いました。
Slackのところは、SlackのApp登録とかスコープの設定とか結構ややこしいです。解説はいろいろなところであるし、Qiitaとかでも解説済みなので、そこをみてやってみてください。
写真撮影の投稿はSlackerなので、簡単に指定チャンネルにアップロードできます。すげーちょろいです。
from slacker import Slacker
slack = Slacker('<bot token>')
slack.files.upload('photos/photo.jpg', channels='#family')
チャンネルもIDじゃなくて名称で指定できてとても楽。
Botの方は、メンションがあったら処理するという流れ。
@app.event("app_mention")
def event_mention(body, say, logger):
# logger.info(body)
files = body['event']['files']
if len(files):
dic = files[0]
name = dic['name']
url = dic['url_private_download']
print('download : {}'.format(url))
savePath = 'fromSlack/{}'.format(name)
if downloadFile(url, savePath):
say("Download Success")
shutter.showImage(savePath)
else:
say("Download Failed")
def downloadFile(url, savePath):
buffer = tempfile.SpooledTemporaryFile(max_size=1e9)
headers={'Authorization': 'Bearer {}'.format(botToken)}
r = requests.get(url, allow_redirects=True, headers=headers, stream=True)
if r.status_code == 200:
downloaded = 0
filesize = int(r.headers['content-length'])
for chunk in r.iter_content(chunk_size=4096):
downloaded += len(chunk)
buffer.write(chunk)
buffer.seek(0)
img = Image.open(io.BytesIO(buffer.read()))
width, height = img.size
scaleW = 720.0/width
scaleH = 720.0/height
scale = max(scaleW, scaleH)
resized = img.resize((int(scale*width), int(scale*height)), Image.BICUBIC)
resized.save(savePath)
return True
該当箇所だけ抜粋ですが、こういう感じで、mentionきたら処理するよって感じでコードが書けます。上記何のエラー処理もしてないので、画像以外のアップロードあったらすぐにコケますが、、
一応デカイサイズの画像がきてもそれをダウンロードして、短辺が720pixelになるように比率守って縮小して保存をしました。ややこしいのは、slackが返すURLはパブリックアクセスができないので、tokenをヘッダにつけてやらないとデータがダウンロードできない点です。
あとは、このSlackBotとカメラ処理のコードを並行で実行することですが、そこは処理が並列で動くわけでもないので、threadingライブラリで組みました。これは疑似スレッドなので、シングルコアしか使いませんが、その分データの受け渡しがラフに書けるので、これを使います。
画面を消したい
一日中画面が光ってるのはちょっと流石に気になります。画面がすぐ壊れそうだし、夜中に光ってるのは気持ちが悪いです。
なので、調べたところ簡単に消したりつけたりできました。
#!/bin/bash
echo 'screen OFF'
sudo sh -c 'echo "1" > /sys/class/backlight/rpi_backlight/bl_power'
echo "1"のところをecho "0"にしたら画面がつきます。このshellスクリプトをsubprocessとかで呼べばいいだけです。
スクリーンセーバー
とはいえ画面はわりと長時間つけておきたいので、最後に撮影した写真や最後にslackで送った写真がずっと出てるのもなーと思いました。なので、ひとまずは最後に撮影した写真と最後にslackで送った写真の2つを交互に表示することにします。画面オフのタイマーと、スクリーンセーバーのタイマーをメインループで実現して完成。
起動はsystemdで
一応起動はsysmtemdでやっときました。User=piを入れないとpythonのライブラリはpiユーザーにしか入れてないので、とかパスの指定を相対にしてるから、カレントディレクトリを移動しないと、とかだけ注意して組み込み完成。
まとめ
すごい長くなりましたが、、、。ソースコードとかCADデータがほしい人いたら連絡ください。問い合わせが多かったら公開します。
こだわりポイントは、正方形のディスプレイ。角丸にスクリーンにマスクしたこと。スクリーンセーバー機能とタイムアウトで画面OFF機能。
そんなところでしょうか。今回の開発にかかった時間は、設計に一晩、スタディに一晩、切削と組み立てに1日(昼間切削回して、夜組み立て)、コーデングに一晩。Pythonでかけるので全体的に簡単です。
今後改善は、スクリーンセーバーで表示する写真を最近撮影した100枚ぐらいまで広げられるようにしようかとか、アップロード先をslackと同時にGooglePhotoかDropboxも加えられるようにしようかとか、いくつかあります。
ギークなお父さんは、ぜひ真似して作ってみてください。