Raspberry Pi Pico WH + MicroPython + 電子ペーパーで、自分好みの卓上カレンダーを作る

本記事は[「Develop fun!」を体現する！ Works Human Intelligence Advent Calendar 2023](https://qiita.com/advent-calendar/2023/advent-works-hi) シリーズ2の17日目の記事です。

## 本記事の概要（TL;DR）

![電子ペーパーで作った卓上カレンダー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/4fb45a6e-c081-3f86-81bb-b855e4fdd71a.jpeg)

- Raspberry Pi Pico WH + MicroPython + 電子ペーパーで卓上カレンダーを作ったよ
- 毎日自動的に日付と日本の祝日をインターネットから取得して、カレンダーに反映するよ
- 電子工作や組み込み系が素人な自分でも、RaspberryPi Picoシリーズを使うことでPythonで比較的お手軽にIoTを体験できたよ

## はじめに

年の瀬になると、つい「来年のカレンダー、どうしようかなあ……」なんて考えてしまいますよね。

筆者は毎年購入するカレンダーに比較的こだわりが強く[^my-calendar-fastidiousness]、なかなか満足できるカレンダーに出会えません。

[^my-calendar-fastidiousness]: 「身の回りのカレンダーは全て月曜始まりに統一したい」「シンプルで視認性が高いものがいい」「日本の祝日が確認できるものがいい」「先月と来月のカレンダーも一緒に確認できるといい」など

更に、紙のカレンダーだとどうしても「今日って何日の何曜日だっけ？」がパッと分からないことが多く、不便に感じることが多々あります。

なので、電子ペーパーとRaspberry Pi Picoを使って、自分が満足できる卓上カレンダーを自作することにしました[^advent-calendar]。

[^advent-calendar]: ~~あとはアドベントカレンダー用の記事ということで、カレンダーを作ったのも正直ちょっとあります~~

## 本記事が目指すゴール

本記事では、下記のゴールを達成することを目的とします。

- Raspberry Pi Pico（以下、Pico）の開発環境をセットアップできる
- Picoに接続した電子ペーパー（[Waveshare 7.5inch E-Paper E-Ink Display Module (B)](https://www.waveshare.com/pico-epaper-7.5-b.htm)）に描画するコードが実行できる
- 任意の月のカレンダーを作成・描画できる
- WiFiからNTPサーバにアクセスし、現在の日付を取得できる
- 日本の祝日をネット上のAPIから取得し、カレンダーに反映できる

## 用意したもの

### Raspberry Pi Pico WH

https://www.switch-science.com/products/8172

こちらのRaspberry Pi Pico WHを購入しました。

Raspberry Pi Zeroなど、他のRasPiシリーズも検討したのですが、

- 現在時刻の取得＋電子ペーパーへのカレンダーの描画ぐらいであればPicoのマイコンでも可能だろうという判断
- 卓上に置くものなので、コンパクトな方が望ましい
- 機能としては電子ペーパーを1日1回書き換えるだけなので、低電力な方が望ましい
- Picoは価格が1000円台からと、非常に安価に購入できて気軽に遊べる

といった理由からPicoにしました。

また、ネットワーク経由で現在の時刻や日本の祝日を取得したかったので、無線LANが使用可能なWを、ピンヘッダ[^pinheader]を自分でハンダ付けするのは自信が無かったので、ピンヘッダが最初からハンダ付けされているHを、という感じでWH選びました。

[^pinheader]: Picoの左右に付いている、ムカデの脚のような金属製の棒状をした接続端子。今回はこれを介して電子ペーパーを制御しています。

### 電子ペーパー

https://www.switch-science.com/products/7318?variant=42382154170566

こちらのPico用電子ペーパーを購入しました。

電子ペーパーを選んだ理由ですが、電子ペーパーは通常のディスプレイに比べて描画の更新に時間が掛かりますが、「1回描画すると、電源を切っても画面上の表示はそのまま維持される」という特性があります。また、バックライト等もないので目に優しく、視野角も広いので、卓上カレンダーにはピッタリだと考えました。

こちらの電子ペーパーには画面サイズのラインナップがあるようですが、カレンダーの文字が大きく表示できるよう、販売されている中で最大サイズの7.5インチを選択しました。

実際に届いてみると、ハガキよりも2回りほど大きいサイズで、卓上カレンダーとしては丁度良いサイズではないかと思います。

また、日曜日と祝日を分かりやすく表示したいので、白黒に加えて赤色も表示できるものにしました。

### その他

その他に必要なケーブルなどは、元々家にあったものを使用するなどしました。

- PCと接続するMicroUSB（USB A - Micro B）ケーブル（家にあったものをそのまま流用）
- スタンドアロンで動作させるためのACアダプタ（いわゆるUSB充電器、家にあったものをそのまま流用）
- 飾るためのケース（100円ショップにて購入、後述）
  - ケースに似合ったMicroUSBケーブル（100円ショップにて購入、後述）

## 開発環境セットアップ

今回は開発用PCにWindows 11、開発言語に[MicroPython](https://micropython.org)、エディタにVS Code + MicroPicoを使用します。

Picoの開発に使用できる言語としては、他にC言語など様々な言語が利用可能なようですが、今回は電子ペーパーの開発元がMicroPython用のコードを提供していることもあり、MicroPythonを使用することにしました。

エディタは使い慣れたVS Codeで開発ができるということで、VS Codeの拡張機能であるMicroPicoを使用しました。

### PicoへのMicroPythonファームウェアの書き込み

[こちらのPico W用ファームウェアのページ](https://micropython.org/download/RPI_PICO_W/)から、MicroPythonファームウェアをダウンロードします。

筆者が使用したのは、記事執筆時点での最新版の[v1.21.0 (2023-10-05) .uf2](https://micropython.org/resources/firmware/RPI_PICO_W-20231005-v1.21.0.uf2)でした。

ダウンロードが完了したら、次にPicoにMicroUSBケーブルを接続し、Picoの**BOOTSELボタンを押しながら**ケーブルをPCと接続します。

接続されると、`RPI-RP2`という名前のUSBストレージとして認識されるはずなので、そのストレージをエクスプローラなどで開きます。

開いたら、先ほどダウンロードしたuf2ファイルをストレージにドラッグ＆ドロップします。

するとUSBが切断＋再接続され、ファームウェアの書き込みが完了します（USBストレージとしては認識されなくなります）。

### VS Codeへの拡張機能のインストール

VSCodeの拡張機能画面から「MicroPico」と検索するか、[こちらのMarketplace](https://marketplace.visualstudio.com/items?itemName=paulober.pico-w-go)から拡張機能をインストールします。

インストールできたら任意の作業に使用したいディレクトリを開き、`Ctrl + Shift + P`などで開くコマンドパレットから
`MicroPico: Configure Project`を選択し、拡張機能に「ここが作業するプロジェクトですよー」と教えてあげましょう。

### 拡張機能の動作確認

拡張機能が動作することを確認するために、試しにPicoに付いているLEDを光らせてみましょう。

拡張機能の説明欄にある"Getting started"に従い、下記のコードをエディタに貼り付けます。

```python
from machine import Pin
from utime import sleep

pin = Pin("LED", Pin.OUT)

print("LED starts flashing...")
while True:
    try:
        pin.toggle()
        sleep(1) # sleep 1sec
    except KeyboardInterrupt:
        break
pin.off()
print("Finished.")
```

VS Codeの最下部にあるステータスバーに「✔ Pico Connected」の表示があるのを確認したら、「▷ Run」をクリックしましょう。

PicoのLEDがチカチカしたら成功です。

### `main.py`について

MicroPythonは、`main.py`という名前で保存されたファイルがあると、電源接続時にその中のコードを自動的に実行するようになります。

試しに先ほどのLEDを光らせるコードを`main.py`という名前で保存し、コマンドパレットから`MicroPico: Upload project to Pico`を選択してみましょう。保存した`main.py`がPico側に保存され、以降はPCに接続しなくても、ACアダプタなどで電力を供給すれば自動的にLEDを光らせるコードが実行されるはずです。

## デモコードを動かす

![raspi_pico_epaper_demo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/b80932b2-cc02-5644-be5b-a26432ca0940.jpeg)

Picoでコードを実行できることが確認できたら、次は電子ペーパーに描画できることを確認してみましょう。

Picoに電子ペーパーを接続したら、[こちらの電子ペーパーの開発元メーカーが公開しているデモコード](https://github.com/waveshareteam/Pico_ePaper_Code/blob/main/python/Pico-ePaper-7.5-B.py)を実行してみます。

しばらく画面のリフレッシュなどが行われ、内容が徐々に描画される[^demo]ので気長に待ち、数十秒ほどで写真のような黒と赤の画面が描画されたら成功です。

[^demo]: 画面が時間を掛けて徐々に描画されるのはこの電子ペーパーの仕様ではなく、デモコードの側で描画とスリープを繰り返しているためです。試しに一番最後以外の`epd.display()`と`epd.delay_ms(5000)`以外をコメントアウトすると、一気に描画されるようになります。

## 実装

ここからはゴリゴリとカレンダーを実装していきましょう。

下記の全てのPythonコードを全て同じディレクトリに配置し、Picoにアップロードします。

### 使用させていただく既存のコード

今回実装するにあたり、いくつかの既存のコードを使用させていただきます。
これらのコードは全てMITライセンスであることを確認済みです。

#### 電子ペーパーを制御する`EPD_7in5_B.py`

https://github.com/waveshareteam/Pico_ePaper_Code/blob/main/python/Pico-ePaper-7.5-B.p

今回は[こちらの電子ペーパーの開発元メーカーが公開しているデモコード](https://github.com/waveshareteam/Pico_ePaper_Code/blob/main/python/Pico-ePaper-7.5-B.py)に実装されている`EPD_7in5_B`クラスをそのまま使用させていただきました。ファイル名は`EPD_7in5_B.py`とし、後述の理由から

```python
import framebuf
```

の部分を

```python
import framebuf2 as framebuf
```

としたほか、245行目の

```python
if __name__=='__main__':
```

以降の行は今回は使用しないため、削除しています。

#### 文字を拡大して描画する`framebuf2.py`

https://github.com/peter-l5/framebuf2

こちらの`framebuf2.py`を使用させていただいています。

MicroPythonの`FrameBuffer`クラスは文字列の描画が8ピクセル固定であり、これだと文字が非常に小さくて見にくい[^framebuffertext]ため、このクラスを拡張して任意の倍率の文字を描画できるようにした`large_text`メソッドを利用させていただきます。

[^framebuffertext]: [デモコードを動かす](#デモコードを動かす)の写真の、左上に書かれている小さな文字のサイズが8ピクセルです。MicroPythonの`FrameBuffer`クラス単体では、このサイズでしか文字を描画できません。

### 今回実装したコード

#### 画面上に描画する文字列や、その描画位置などの情報を持つ`DrawableString.py`
```DrawableString.py
class DrawableString:
    def __init__(self, x: int, y: int, string: str, width: int, height: int,
                 size_multiply: int = 1, is_red: bool = False, is_filled: bool = False) -> None:
        self.x = x
        self.y = y
        self.string = string
        self.width = width
        self.height = height
        self.size_multiply = size_multiply
        self.is_red = is_red
        self.is_filled = is_filled

```

#### カレンダーを計算し、`DrawableString`のリストを返す`Calendar.py`

MicroPythonにはCPythonなど一般的なPythonの標準ライブラリにあるような`datetime`クラスが無いため、うるう年の判定や月あたりの日数、曜日の計算を自力で行っています。

```Calendar.py
from DrawableString import DrawableString


class Calendar:
    __FONT_BASIC_WIDTH = 8
    __FONT_BASIC_HEIGHT = 8
    __MARGIN_BASIC_LEFT = 4
    __MARGIN_BASIC_BOTTOM = 4

    def __init__(self, year: int, month: int, today: int, x: int, y: int, size_multiply: int,
                holidays: list, additional_margin_left: int = 0,
                additional_margin_bottom: int = 0) -> None:
        self.__year = year
        self.__month = month
        self.__today = today
        self.__x = x
        self.__y = y
        self.__size_multiply = size_multiply
        self.__holidays = Calendar.__get_holiday_day_list(
            year, month, holidays)
        self.__additional_margin_left = additional_margin_left
        self.__additional_margin_bottom = additional_margin_bottom

    def __get_holiday_day_list(year: int, month: int, holidays: list) -> list:
        days = []
        for date in holidays:
            date_year, date_month, date_day = date.split('-')
            if date_year == str(year) and date_month == f"{month:02}":
                days.append(int(date_day))
        return days

    def __get_is_leap_year(year: int) -> bool:
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

    def __get_days_in_month(year: int, month: int) -> int:
        if month in [4, 6, 9, 11]:
            return 30
        elif month == 2:
            return 29 if Calendar.__get_is_leap_year(year) else 28
        else:
            return 31

    def __get_day_of_week_number(year: int, month: int, day: int) -> int:
        # See: [Zeller's congruence](https://en.wikipedia.org/wiki/Zeller%27s_congruence)
        if month < 3:
            month += 12
            year -= 1

        k = year % 100
        j = year // 100

        h = day + ((13 * (month + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)
        return (h + 5) % 7

    def generate(self) -> list:
        first_weekday_no = Calendar.__get_day_of_week_number(
            self.__year, self.__month, 1)
        days = Calendar.__get_days_in_month(self.__year, self.__month)

        today = self.__today

        x = self.__x
        y = self.__y

        char_size = self.__size_multiply
        char_width = self.__FONT_BASIC_WIDTH * self.__size_multiply
        char_height = self.__FONT_BASIC_HEIGHT * self.__size_multiply

        margin_left = self.__MARGIN_BASIC_LEFT * \
            self.__size_multiply + self.__additional_margin_left
        margin_bottom = self.__MARGIN_BASIC_BOTTOM * \
            self.__size_multiply + self.__additional_margin_bottom

        result = []

        weekday_abbr = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
        weekday_str_size = 2

        width = char_width * weekday_str_size + margin_left
        height = char_height + margin_bottom

        for wd in weekday_abbr:
            result.append(DrawableString(x, y, wd, char_width * weekday_str_size,
                                         char_height, char_size, True if wd == "Su" else False, False))
            x += width

        x = self.__x + width * first_weekday_no
        y += height

        for day in range(1, days + 1):
            weekday_no = (first_weekday_no + day - 1) % 7
            is_red = True if weekday_no == 6 or day in self.__holidays else False
            is_filled = True if today is not None and day == today else False
            result.append(DrawableString(
                x, y, f"{day:2d}", char_width * 2, char_height, char_size, is_red, is_filled))

            x += width

            if weekday_no == 6:
                x = self.__x
                y += height

        return result

```

#### NTPサーバから現在日時を取得する`DateTimeGetter.py`

NTPサーバには任意のサーバを指定できるようになっています。
今回は[NICT](https://www.nict.go.jp/index.html)のNTPサーバを利用させていただいています。

```DateTimeGetter.py
import network
import time
import ntptime


def local_date_time_getter(ssid: str, password: str,
                            host: str = "ntp.nict.jp",
                            offset_min: int = 9 * 60) -> time.struct_time:
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(ssid, password)

    while not wlan.isconnected():
        print("Waiting for connection...")
        time.sleep(1)

    ntptime.host = host
    ntptime.settime()
    return time.localtime(time.time() + offset_min * 60)

```

#### 日本の祝日を取得する`HolidayGetter.py`

有志の方が公開されている、[Holidays JP API](https://holidays-jp.github.io)というとても便利なAPIを利用させていただきました。

```HolidayGetter.py
import network
import time
import requests


def get_holiday_json(ssid: str, password: str):
    URL = "https://holidays-jp.github.io/api/v1/date.json"

    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(ssid, password)

    while not wlan.isconnected():
        print("Waiting for connection...")
        time.sleep(1)

    return requests.get(URL).json()


def get_holidays_date(ssid: str, password: str) -> list:
    json = get_holiday_json(ssid, password)
    return list(json.keys())

```

#### WiFiのSSID / パスワードを保存する`secret.py`

プロジェクトをGitで管理する場合、間違ってGitHubなどに上げてしまわないよう、`.gitignore`ファイルに`secret.py`の1行を追加しておきましょう。

```secret.py
SSID = "接続したいWiFiのSSID"
PASSWORD = "接続したいWiFiのパスワード"

```

#### エントリポイントとなる`main.py`

割とごちゃっとしてしまったので、もっとファイルを分割すべきだったかも知れません。

カレンダーなど各パーツの配置は、それぞれのパーツの大きさを手で計算しながら地道に配置＆調整しました。

```main.py
import machine
import time

from EPD_7in5_B import EPD_7in5_B
import framebuf2 as framebuf
from Calendar import Calendar
from DateTimeGetter import local_date_time_getter
from secret import SSID, PASSWORD
from HolidayGetter import get_holidays_date


def draw_calendar(image_black: framebuf.FrameBuffer, image_red: framebuf.FrameBuffer,
                  calendar_drawable_string_list: list) -> None:
    for s in calendar_drawable_string_list:
        if s.is_filled:
            if s.is_red:
                image_red.rect(s.x - s.width//8, s.y - s.height//4, s.width + s.width//4,
                               s.height + s.height//2, 0xff, True)
                image_red.large_text(
                    s.string, s.x, s.y, s.size_multiply, 0x00)
            else:
                image_black.rect(s.x - s.width//8, s.y - s.height//4, s.width + s.width//4,
                                 s.height + s.height//2, 0x00, True)
                image_black.large_text(
                    s.string, s.x, s.y, s.size_multiply, 0xff)
        else:
            if s.is_red:
                image_red.large_text(
                    s.string, s.x, s.y, s.size_multiply, 0xff)
            else:
                image_black.large_text(
                    s.string, s.x, s.y, s.size_multiply, 0x00)


def set_rtc_from_ntp(rtc, led, ssid: str, password: str) -> (int, int, int, int) | None:
    year = 2000
    month = 1
    day = 1
    hour = 0
    min = 0
    sec = 0
    weekday_num = 5

    try:
        toggle_led(led, True)
        (year, month, day, hour, min, sec, weekday_num,
         _) = local_date_time_getter(ssid, password, "ntp.nict.jp", 9 * 60)
        print(
            f"Successfully got datetime: {year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}")
        toggle_led(led, False)
    except Exception as e:
        print(f"Failed to get datetime: {e}")
        blink_led(led)
        return None

    try:
        rtc.datetime((year, month, day, weekday_num, hour, min, sec, 0))
        print(f"Successfully set RTC: {rtc.datetime()}")
    except Exception as e:
        print(f"Failed to set RTC: {e}")
        blink_led(led)
        return None

    return (year, month, day, weekday_num)


def set_rtc_from_ntp_with_retry(rtc, led, ssid: str, password: str) -> (int, int, int, int) | None:
    SLEEP_SEC = 5
    RETRY_TIMES = 5

    result = None
    for i in range(RETRY_TIMES):
        result = set_rtc_from_ntp(rtc, led, ssid, password)
        if result:
            break
        print("Setting RTC from NTP seems to have been failed...")
        print(f"Retry in {SLEEP_SEC} seconds. ({i+1}/{RETRY_TIMES})")
        time.sleep(SLEEP_SEC)

    return result


def get_holidays(led, ssid: str, password: str) -> list | None:
    holidays = None

    try:
        toggle_led(led, True)
        holidays = get_holidays_date(ssid, password)
        print(f"Successfully got holidays: {holidays}")
        toggle_led(led, False)
    except Exception as e:
        print(f"Failed to get holidays: {e}")
        blink_led(led)

    return holidays


def get_holidays_with_retry(led, ssid: str, password: str) -> list | None:
    SLEEP_SEC = 5
    RETRY_TIMES = 5

    result = None
    for i in range(RETRY_TIMES):
        result = get_holidays(led, ssid, password)
        if result:
            break
        print("Getting holidays seems to have been failed...")
        print(f"Retry in {SLEEP_SEC} seconds. ({i+1}/{RETRY_TIMES})")
        time.sleep(SLEEP_SEC)

    return result


def toggle_led(led, is_on: bool) -> None:
    if is_on:
        led.on()
    else:
        led.off()


def blink_led(led) -> None:
    for i in range(10):
        toggle_led(led, True if i % 2 == 0 else False)
        time.sleep_ms(100)


def draw_main(epd: EPD_7in5_B, date: (int, int, int, int), holidays: list) -> None:
    (year, month, day, weekday_num) = date or (2000, 1, 1, 5)

    if not holidays:
        holidays = []

    epd.Clear()

    epd.imageblack.fill(0xff)
    epd.imagered.fill(0x00)

    weekday_abbr = ["Mon", "Tue", "Wed", "Thu",
                    "Fri", "Sat", "Sun"][weekday_num]
    epd.imageblack.large_text(
        f"{year:04}-", 16, 38, 3, 0x00)
    epd.imageblack.large_text(
        f"{month:02}-{day:02} {weekday_abbr}", 136, 16, 6, 0x00)
    print("Completed drawing of today's date.")

    epd.imageblack.hline(16, 80, 768, 0x00)
    print("Completed drawing of horizontal line.")

    this_month_calendar = Calendar(
        year, month, day, 16, 96, 3, holidays, 12, 6)
    draw_calendar(epd.imageblack, epd.imagered,
                  this_month_calendar.generate())

    print("Completed drawing of this month's calendar.")

    epd.imageblack.vline(504, 80, 384, 0x00)
    print("Completed drawing of vertical line.")

    prev_year = year if month > 1 else year - 1
    prev_month = month - 1 if month > 1 else 12

    epd.imageblack.large_text(
        f"{prev_year:04}-{prev_month:02}", 512, 96, 2, 0x00)

    prev_calendar = Calendar(prev_year, prev_month,
                             None, 512, 120, 2, holidays)
    draw_calendar(epd.imageblack, epd.imagered, prev_calendar.generate())

    print("Completed drawing of last month's calendar.")

    next_year = year if month < 12 else year + 1
    next_month = month + 1 if month < 12 else 1
    epd.imageblack.large_text(
        f"{next_year:04}-{next_month:02}", 512, 288, 2, 0x00)

    next_calendar = Calendar(next_year, next_month,
                             None, 512, 312, 2, holidays)
    draw_calendar(epd.imageblack, epd.imagered, next_calendar.generate())

    print("Completed drawing of next month's calendar.")

    epd.display()
    print("Drawing done.")
    epd.sleep()


def do_main_sequence(rtc, ssid: str, password: str) -> (int, int, int, int):
    epd = EPD_7in5_B()
    led = machine.Pin("LED", machine.Pin.OUT)

    calendar_date = set_rtc_from_ntp_with_retry(
        rtc, led, ssid, password) or (2000, 1, 1, 5)
    holidays = get_holidays_with_retry(led, ssid, password) or []
    draw_main(epd, calendar_date, holidays)

    return calendar_date


if __name__ == '__main__':
    rtc = machine.RTC()
    calendar_date = do_main_sequence(rtc, SSID, PASSWORD)

    SLEEP_MINUTES = 10
    while True:
        rtc_datetime = rtc.datetime()
        if (calendar_date[0] != rtc_datetime[0]
                or calendar_date[1] != rtc_datetime[1]
                or calendar_date[2] != rtc_datetime[2]
                ):
            print("Redraw as the date seems to have changed.")
            calendar_date = do_main_sequence(rtc, SSID, PASSWORD)
        else:
            print(f"RTC datetime: {rtc_datetime[0]}-{rtc_datetime[1]}-{rtc_datetime[2]} " +
                  f"{rtc_datetime[4]}:{rtc_datetime[5]}:{rtc_datetime[6]}")
            print(
                f"Showing calendar date: {calendar_date[0]}-{calendar_date[1]}-{calendar_date[2]}")
            print(f"Sleep {SLEEP_MINUTES} minutes until the date changes...")
            time.sleep(SLEEP_MINUTES * 60)

```

## 実行してみる

![カレンダーの実装が完了し、実行してみました](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/2fa7ecd0-e6f9-682b-19a9-9e50c86509b1.jpeg)

実行すると、VSCodeのターミナル（REPL）に現在の状況が出力されます。

また、Pico本体もネットワーク通信を行っている間はLEDが光ります（エラーが起きると点滅し、各5回までリトライします）。

実行してしばらく待っていると、写真のように現在の日付と今月のカレンダー、右側に小さめに先月と来月のカレンダーが描画されます。

カレンダーは今日の日付の部分について色が反転表示されて分かりやすくなっているほか、日曜日と日本の祝日は赤色で描画されています。

2023年12月は残念ながら祝日が無い:sob:ので分かりにくいですが、写真右側の先月来月のカレンダーを見ると、11月3日の文化の日、23日の勤労感謝の日、2024年1月1日の元日、8日の成人の日が赤く表示されていることが分かります。

以上で、[本記事のゴール](#本記事が目指すゴール)が達成できたことが確認できました。

## ケースの作成

![epaper_calendar.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/5ecf37d7-28a5-19c6-16ae-15fdfff20c81.jpeg)

現在のままでは基板等が剥き出しで、机の上に置くことができません。
なので、良さそうな額縁やケースを100円ショップで探したところ、下記の商品が電子ペーパーのサイズにピッタリでした。

https://jp.daisonet.com/collections/interior0203/products/4549131922165

この木箱のフタ部分にマスキングテープで電子ペーパーを取り付け、側面に木工用ドリルで穴を空けてUSBケーブルを通して、中にRaspPi Picoなどを収納したところ、良い感じのケースになりました。

また、せっかくなのでケーブルも木に合いそうなゴールドカラーのものを同様に100円ショップで購入しました。

https://jp.daisonet.com/products/4549131856132

## おわりに

筆者は今回扱ったような電子工作や組み込み系、マイコンといったジャンルには全く詳しくないのですが、そんな素人同然の自分でも、普段比較的親しみのあるPythonを使って、あまり迷うことなく目的のコードを書くことができました。

このカレンダーも、アイディア次第で

- もっと省電力化して、バッテリ駆動にしてみる
- 動作の安定性を高める
- 天気予報やGoogleカレンダーの予定を表示してみる

のような、発展や工夫の余地があると感じています。これらにも挑戦してみたいですね。

またそれ以外にも、Picoは非常に安価で気軽に買いやすいので、これからも色々作ってみたいですね。

今年のアドベントカレンダーも[例](https://qiita.com/h53/items/b6fda6a3ccf77303cefe)[年](https://qiita.com/h53/items/7e6b3fe4e3b11e0bf016)に引き続き、業務とは一切関係ない自分の趣味全開の記事になってしまいましたが、自分自身がとても楽しんで開発を行うことができました。

この記事が皆様の創作意欲を刺激し、ひいては「Develop fun!」に繋がる、そんな楽しいエンジニアライフの一助になれば幸いです。

## 参考文献

- [Pico e-Paper 7.5 B - Waveshare Wiki](https://www.waveshare.com/wiki/Pico-ePaper-7.5-B)
- [waveshareteamPico_ePaper_Code Waveshrae Pico e-Paper driver code](https://github.com/waveshareteam/Pico_ePaper_Code)
- [MicroPython ライブラリ — MicroPython latest ドキュメント](https://micropython-docs-ja.readthedocs.io/ja/latest/library/index.html)
- [Raspberry Pi Pico W の LED を VS Code を使って、点滅させてみた。（RP-Pico チュートリアル）](https://qiita.com/IoriGunji/items/ebe6cefd3562639f7ca2)
- [Raspberry Pi Pico W を Wi-Fi に接続してみる。](https://qiita.com/IoriGunji/items/7ed9a09c03d5a9693ca0)