この記事は2481文字約7分で読めます

ラズパイが余っているので活用したいと思います。

Table of Contents

ラズパイ有効活用

皆さんは家に余っているラズパイはありますか?

私はあります...。

いい加減眠らせておくのももったいなく感じ、かつちょうどカレンダーが家になかった(会社でもらったカレンダーは全部捨ててしまった)のでラズパイと電子ペーパーを使って万年カレンダーでも作ってみようかと思います。

ラズパイと電子ペーパーのセットアップは過去記事MQTTと電子ペーパーを使って年賀状を作るで実施済みなのでこちらを使うことにします。

そもそもなぜ電子ペーパー?

昨今リモートワークも多くなり、オフィスに出向くことも少なくなりました。私はオフィスの机に卓上カレンダーを置いていたのですがリモートワーク中でもカレンダーが常に見れるようにしておきたいです。なぜなら無茶なスケジュールをクライアントから要求されたときに、おおよそ何週間くらい猶予があるのかカレンダーを使うことで直感的にわかるからです。

しかしながら家に置いておく卓上カレンダーが現在ありません。

買えばいいじゃんという意見も出てきそうですが過去記事の通り、せっかくWaveshare 2.7inch E-Ink Screen Display HAT for Raspberry Piを買ったのでカレンダーデバイスを作ってみようと思います。

先人がいました。

作ろうと思って調べるとすでに電子ペーパーでカレンダーを作っていた人がいました。しかもGoogleカレンダー連携までしているまさにスマートデバイス。

Googleと連動した電子ペーパー製カレンダーを作る

一からコードを書くのもめんどくさくなり、上記をパクッて参考に作ればいいという気分になりました。

コード自体は結構コピペで作って、画面サイズ変更と赤色の印字無効だけで済ませてしまったので公開はしませんが詰まったところを下記に書いていきたいと思います。

詰まったところその1 GoogleのoAuth2.0認証が取れない!!

これが一番苦戦しました。

Googleと連動した電子ペーパー製カレンダーを作るを参考に作っていくとGoogleカレンダーとの連携のため、oAuth2.0のユーザー認証が必要なのですが...。

Google CloudのAPIコンソールから発行できるクレデンシャル用のJSONがある場合で、認証済みの場合に作成されるtoken.pickleが存在しない場合はgoogle_auth_oauthlib.flow.InstalledAppFlowのrun_local_serverを使って、ローカル上にWebサーバーを立ててGoogleアカウントでの認証が済んだ場合にローカルサーバーにリダイレクトするようにする認証方式をとるようにしてありました。

        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    'credentials.json', SCOPES)
                creds = flow.run_local_server(port=0)
            # Save the credentials for the next run
            with open('token.pickle', 'wb') as token:
                pickle.dump(creds, token)

公式のドキュメントに載っているクイックスタートでも同様の設定になっているので問題ないと思ってましたが....。

いざリダイレクトが始まると..。

エラー 400: redirect_uri_mismatch The redirect URI in the request, http://localhost:58979/, does not match the ones authorized for the OAuth client. To update the authorized redirect URIs, visit: https://console.developers.google.com/apis/credentials/oauthclient/xxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com?project=xxxxxxxxxxxxx

というエラーがブラウザで出てしまい、リダイレクトされず当然認証も通りませんでした。

エラー内容的にoAuthのリダイレクトURLがミスマッチということなので、リダイレクトURLに http://localhost つまり、http://localhost:80 を設定していたのが悪いのではという仮説のもと、flow.run_local_server() のポートを port=xxxxxで固定化してリダイレクトURLもポート指定にして再度トライしたのですが、同じエラーでした。

途方に暮れていたのですが、そもそも自己でリダイレクト先のローカルサーバーを立てることのできないタイプのデバイスもあるはずでそういった場合はどうやって認証するのか調べたところ...。

テレビと入力機能が限られているデバイスという認証オプションが用意されてました。

APIのコンソールでoAuth2.0のクライアント作成時に上記を選んだ後、コード側で、

creds = flow.run_local_server(port=0)
↓
creds = flow.run_console()

に書いてあげることで、ブラウザ上で認証が完了したらワンタイムコードが表示され、それをコンソール上に入力する形で認証が通るようになりました。めでたしめでたし。

詰まったところその2 2.7インチは画面小さすぎ

先人の知恵をお借りして画面サイズとフォントサイズだけ合わせと赤色無効化をして調整して作ったところ...。

tiisai

小さいっ!圧倒的に小さい。Zippoと比べてもらうとわかるのですが、こんなの使いものにならない!

ima

先人、かなり理にかなったデザインをしていたようで、メインのカレンダーの他、右側に予定表と次の月のミニカレンダーを表示するようになっているのですが、さすがに2.7インチには小さすぎました。

さすがにこのサイズだと予定表とか読めたものではないです。もちろん削ってしまってもいいのですがせっかく苦労してGoogleカレンダーと連携しているので何とかしたい...!

自慢

予定表をよく見てほしい。

imfg

そう。今年色々大変そうですがBanG Dream! 9th☆LIVE「The Beginning」行きます。

楽しみだなぁ。

GPIOのキースイッチを使おう!

話を戻して、2.7インチだとさすがにいろいろ視認性が悪いですね、という結論になりました。

そこで、2.7インチHATに搭載されているキースイッチを活用することにしました。

img

キースイッチは4つありますので、4画面に分割できます。これならうまくいきそうですね。

とりあえず下記の通りにしました。

  • Key1: 当月カレンダー
  • Key2: 来月カレンダー
  • Key3: 当月カレンダー&前月・来月カレンダー
  • Key4: 予定表

とします。

それぞれを分割してスクリプトを作成します。コードは上記でも申しているとおり、コピペが多いのでさすがに恥ずかしいので載せません..!

詰まったところその3 キースイッチがデータシートに載ってない!

さて、それぞれの描画スクリプトは何のことなく作れたので、あとはキースイッチが押されたら画面を切り替えるようにします。

ではデータシートを見ていきましょう。https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT

あれ?キースイッチの記載がない...。

ちゃんと書いてよぉ...。と思いましたがhttps://diyprojects.io/test-waveshare-epaper-eink-2-7-spi-screen-raspberry-pi-python/というサイトに、

  • Key 1 on pin 12
  • Key 2 on pin 16
  • Key 3 on pin 18
  • Key 4 on pin 4

という記載がありました!!GoodJob!

ということでコードを書いていきます。

#!/usr/bin/env python3
import subprocess
from datetime import datetime
import time
import RPi.GPIO as GPIO


BOUNCE_TIME = 500


PIN_KEY1 = 5
PIN_KEY2 = 6
PIN_KEY3 = 13
PIN_KEY4 = 19

DISPMODE_THIS_MONTH = 1
DISPMODE_NEXT_MONTH = 2
DISPMODE_BEFORE_NEXT = 3
DISPMODE_TODO = 4


class Display:
    mode = DISPMODE_THIS_MONTH

    def start(self, first_mode=DISPMODE_THIS_MONTH):
        self.mode = first_mode
        while True:
            if DISPMODE_NEXT_MONTH == self.mode:
                self.draw_next_month()
            elif DISPMODE_BEFORE_NEXT == self.mode:
                self.draw_before_next()
            elif DISPMODE_TODO == self.mode:
                self.draw_todo()
            else:
                self.mode = DISPMODE_THIS_MONTH
                self.draw_this_month()
            time.sleep(3600)

    def key_pressed(self, pin):
        if PIN_KEY1 == pin:
            self.mode = DISPMODE_THIS_MONTH
            self.draw_this_month()
        elif PIN_KEY2 == pin:
            self.mode = DISPMODE_NEXT_MONTH
            self.draw_next_month()
        elif PIN_KEY3 == pin:
            self.mode = DISPMODE_BEFORE_NEXT
            self.draw_before_next()
        elif PIN_KEY4 == pin:
            self.mode = DISPMODE_TODO
            self.draw_todo()
        else:
            print(pin)
    
    @staticmethod
    def draw_this_month():
        command = ["python3", "/home/pi/e-calendar/this_month.py"]
        print(command)
        proc = subprocess.Popen(command)
        result = proc.communicate()
        print(result)

    @staticmethod
    def draw_next_month():
        command = ["python3", "/home/pi/e-calendar/next_month.py"]
        print(command)
        proc = subprocess.Popen(command)
        result = proc.communicate()
        print(result)

    @staticmethod
    def draw_before_next():
        command = ["python3", "/home/pi/e-calendar/before_next.py"]
        print(command)
        proc = subprocess.Popen(command)
        result = proc.communicate()
        print(result)

    @staticmethod
    def draw_todo():
        command = ["python3", "/home/pi/e-calendar/todo.py"]
        print(command)
        proc = subprocess.Popen(command)
        result = proc.communicate()
        print(result)


if __name__ == '__main__':
    try:
        display = Display()
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(PIN_KEY1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(PIN_KEY1, GPIO.FALLING, callback=display.key_pressed, BOUNCE_TIME=BOUNCE_TIME)
        GPIO.setup(PIN_KEY2, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(PIN_KEY2, GPIO.FALLING, callback=display.key_pressed, BOUNCE_TIME=BOUNCE_TIME)
        GPIO.setup(PIN_KEY3, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(PIN_KEY3, GPIO.FALLING, callback=display.key_pressed, BOUNCE_TIME=BOUNCE_TIME)
        GPIO.setup(PIN_KEY4, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(PIN_KEY4, GPIO.FALLING, callback=display.key_pressed, BOUNCE_TIME=BOUNCE_TIME)
        display.start(DISPMODE_THIS_MONTH)
    finally:
        GPIO.cleanup()

ポイントは、キースイッチはただのGPIOなので、RPi.GPIO を使って制御できます。

キースイッチが押されたときに何かをする、というのはGPIO.add_event_detectでイベントリスナーを登録できます。コールバック関数が設定できるので、こちらにキースイッチが押下された際のふるまいを登録すればいいです。また、コールバック関数にはGPIOのPINが引数でわたりますのでkey_pressedみたいな感じで共通化できます。

本来はちゃんと作るべきなのですが、ちょっと手探りで画面を作っていたところもあり、subprocessを使って直接Pythonファイルを実行するイケてない実装になってます。恥ずかしい。

できた!

できました。

当月カレンダー↓

mga

翌月カレンダー↓

imng

当月カレンダー&前月・来月カレンダー↓

imna

予定表↓

ima

電子ペーパーの再描画はかなり重たいので切り替えには時間がかかるのが欠点ですが他はまぁまぁ上出来ではないでしょうか?

おっそ。

tubone24にラーメンを食べさせよう!

ぽちっとな↓

Buy me a ramen
hatena bookmark