【近未来的】ラズパイで透明OLEDディスプレイを使いこなそう

【PR】この記事には広告が含まれています。

【近未来的】ラズパイで透明OLEDディスプレイを使いこなそう

数あるラズパイ用ディスプレイの中でも、トップクラスの近未来感を放つ透明ディスプレイ。コンパクトながらも「つい見とれてしまう」魅力的なアイテムです。

本記事ではRaspberry Piを使い、透明ディスプレイにさまざまな情報を表示するためのテクニックを紹介します。

今回使用する1.51インチ透明 青色OLEDディスプレイ(128×64 SPI/I2C)は、スイッチサイエンスで購入できます。メーカーは円形ディスプレイ電子ペーパーなどを製造しているWaveshareです。

透明ディスプレイを使う準備

まずは透明ディスプレイをラズパイから操作するための準備をします。

透明ディスプレイ側の準備

透明ディスプレイとコントローラーをケーブルで接続します。

ラズパイ側の準備

使用するRaspberry Piの準備をします。本記事内のコードは、以下のモデルで動作確認をしています。

チェックポイント

まだラズパイをお持ちでない方は、Raspberry Pi 3 A+がおすすめです。透明ディスプレイを動かすには、Raspberry Pi 4だと少しオーバースペックに感じます。性能的にはRaspberry Pi Zero 2Wも適任ですが、ピンヘッダーをはんだ付けしないと透明ディスプレイを接続できないことがネックです。

Raspberry Pi 5では従来のGPIO制御ライブラリが互換性を持たないため、この記事で紹介するコードを動かすことができません。

OSは以前のバージョンであるBullseyeを使用しました。

OSをインストールする方法は以下の記事で詳しく解説しています。
≫【2024年最新版】OSインストールから初期設定まで|セットアップ手順のすべて

ラズパイと透明ディスプレイの接続

ラズパイと透明ディスプレイは以下のように配線します。配線作業時はラズパイ本体の電源を切りましょう。付属のワイヤは色分けされているため、色に従って簡単に配線ができます。

出典:WaveShareの公式サイト

接続が完了したら、Raspberry Piを起動します。

SPIを有効にする

Raspberry Piと透明ディスプレイは、SPIを通じて通信します。SPI(Serial Peripheral Interface)はデバイス間で、データを通信するためのインターフェースです。初期設定ではSPIが使える状態になっていないため、以下の手順で設定します。

スタートメニューから「設定」→「Raspberry Piの設定」の順に展開します。

SPIの項目をクリックして有効にします。

設定を有効にしたら、Raspberry Piを再起動します。

ライブラリのインストール

透明ディスプレイを制御するためのライブラリをRaspberry Piにインストールします。ライブラリは複数の機能をひとまとめにしたものであり、プログラムから簡単に呼び出して使うことができます。

メーカー提供のライブラリをRaspberry Pi 4で使うと、透明ディスプレイが表示されない問題が確認されたため、代わりにLumaライブラリを使用します。LumaライブラリはPythonを使用してOLEDやLEDマトリクスなどのディスプレイデバイスを制御するためのツールキットです。

次のコマンドでlumaライブラリをインストールします。

sudo pip3 install luma.oled

透明ディスプレイに日本語を表示させるため、Takaoフォントをインストールします。

sudo apt install fonts-takao

インストールの途中で「続行しますか?」と聞かれるので、yを入力して、Enterキーを押します。


以下のコマンドを使用してバージョン9.5.0のPillowをインストールします。

sudo pip3 install Pillow==9.5.0

最新バージョンのPillowライブラリでは、getsizeメソッド(テキストの表示に必要なピクセルサイズを取得するためのメソッド)が削除されているため、バージョン指定でインストールします。

表示テストをしてみよう

透明ディスプレイに3Dアニメーションを表示するためのプログラムを実行してみましょう。

スタートメニューの「プログラミング」から「Thonny」を開きます。ThonnyはPythonの統合開発環境(IDE)で、コードの記述、実行、保存などを行うことができます。

以下がThonnyの画面です。後述のコードをコピーペーストして「Run」ボタンを押すと、プログラムが実行されます。

3Dアニメーションを表示するコード

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from luma.core.render import canvas
import RPi.GPIO as GPIO
from PIL import Image, ImageDraw
from math import sin, cos, radians
import time

GPIO.setwarnings(False)

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# スケーリング係数とX軸のオフセット
scale_factor = 0.8
x_offset = 65

# 画面中心の定数
X_CENTER = 64
Y_CENTER = 32

# 三角錐の頂点座標
pyramid = ((0, -20, 0), (-20, 20, -20), (20, 20, -20), (0, 20, 20))

try:
    while True:
        for angle in range(0, 361, 10):
            f = [[0.0 for _ in range(3)] for _ in range(4)]
            r = radians(angle)

            for i in range(4):
                x1 = pyramid[i][2] * sin(r) + pyramid[i][0] * cos(r)
                ya = pyramid[i][1]
                z1 = pyramid[i][2] * cos(r) - pyramid[i][0] * sin(r)

                y2 = ya * cos(r) - z1 * sin(r)
                z2 = ya * sin(r) + z1 * cos(r)

                x3 = x1 * cos(r) - y2 * sin(r)
                y3 = x1 * sin(r) + y2 * cos(r)

                x3 = (x1 * cos(r) - y2 * sin(r)) * scale_factor + x_offset
                y3 = (x1 * sin(r) + y2 * cos(r)) * scale_factor + Y_CENTER

                f[i][0] = x3
                f[i][1] = y3

            with canvas(device) as draw:
                for i in range(3):
                    draw.line((f[0][0], f[0][1], f[i + 1][0], f[i + 1][1]), fill="white")
                    draw.line((f[i + 1][0], f[i + 1][1], f[(i + 1) % 3 + 1][0], f[(i + 1) % 3 + 1][1]), fill="white")
            time.sleep(0.1)

except KeyboardInterrupt:
    # キーボード割り込み(Ctrl+Cなど)が発生した場合に実行される
    GPIO.cleanup() # 使用していたGPIOピンをクリーンアップして、デフォルトの状態(未使用状態)に戻す

このプログラムは透明ディスプレイに三角錐の回転アニメーションを表示するものです。まず、SPI接続を設定してディスプレイと通信します。

三角錐の頂点を計算し、それをスケーリングしてディスプレイの特定の位置に合わせます。三角錐は連続して異なる角度から描画され、それによって立体的に回転しているように見えるアニメーションが生成されます。

各頂点の座標は三角関数を用いて計算され、その結果に基づいてディスプレイ上で線が描かれます。この処理は無限ループ内で実行され、ユーザーがキーボード割り込み(Ctrl+C)を行うまで続けられます。割り込みが発生すると、GPIOピンの設定がクリーンアップされ、プログラムが終了します。

プログラムを実行しても透明ディスプレイに何も表示されない場合、配線の接続ミスが考えられます。もう一度、ワイヤーが正しいピンに接続されているかを確認してみましょう。

文字を表示してみよう

次は文字の表示方法を紹介します。

文字を1行ずつ表示

以下のコードは透明ディスプレイにテキストを表示するためのものです。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from PIL import ImageFont, ImageDraw, Image
import RPi.GPIO as GPIO

GPIO.setwarnings(False)

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# フォントのパスとサイズ設定
font_path = "/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf"
font_size = 11
font = ImageFont.truetype(font_path, font_size)

# 新しい画像オブジェクトを作成
image = Image.new('1', (device.width, device.height))

# 画像に描画するためのDrawオブジェクトを作成
draw = ImageDraw.Draw(image)

# テキストを描画
draw.text((0, 0), "1.51インチ透明", font=font, fill=255)
draw.text((5, 30), "青色OLEDディスプレイ", font=font, fill=255)

# 描画した内容をディスプレイに表示
device.display(image)

Takaoフォントを使用するために、フォントファイルのパスを指定し、文字のサイズとして11ピクセルを選択します。文字サイズの数値(font_size = 11)は自由に変更できます。

新しい画像オブジェクトを作成した後、この画像上でDrawオブジェクトを使ってテキストを描画します。テキストの位置はxとyの座標を使って指定します。座標の指定方法は、ディスプレイの左上を0として考えます。

テキスト「1.51インチ透明」はx=0、y=0の位置に配置され、これは画像の左上からスタートすることを意味します。次のテキスト「青色OLEDディスプレイ」はx=5、y=30の位置に表示されます。

長文をスクロールして表示

先ほどのコードでは、1行の表示範囲を超える長さの文を表示することができません。そこで、文字を自動的に改行やスクロールしながら表示する方法を紹介します。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from luma.core.render import canvas
from PIL import ImageFont, Image
import RPi.GPIO as GPIO
import time

GPIO.setwarnings(False)

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# フォントのパスとサイズ設定
font_path = "/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf"
font_size = 11
font = ImageFont.truetype(font_path, font_size)

# テキストの内容
text = "こんにちは、Raspberry Piと透明OLEDディスプレイを使っています。文字がディスプレイの幅を超えた場合は自動的に改行やスクロールを行い、長いテキストも適切に扱えるようにしました。"

# 行の高さ
line_height = font_size + 2

# ディスプレイをクリアして初期化
device.clear()
time.sleep(0.5)  # 5秒待機

def draw_text_with_scroll(text, font, device, line_height):
    lines = []
    for char in text:
        # 新しい文字を追加してテキストの幅を計算
        if lines:
            current_text = lines[-1] + char
        else:
            current_text = char
        text_width, _ = font.getsize(current_text)
        
        # 幅がディスプレイを超える場合、新しい行を開始
        if text_width > device.width:
            lines.append(char)
        else:
            if not lines:
                lines.append(current_text)
            else:
                lines[-1] = current_text

        # ディスプレイの高さを超えたらスクロール
        if len(lines) * line_height > device.height:
            # スクロールさせるために、最初の行を削除
            lines.pop(0)

        # ディスプレイにテキストを表示
        with canvas(device) as draw:
            y_position = 0
            for line in lines:
                draw.text((0, y_position), line, font=font, fill="white")
                y_position += line_height
        time.sleep(0.04)

draw_text_with_scroll(text, font, device, line_height)

draw_text_with_scroll関数では、与えられたテキストを一文字ずつ処理し、現在の行のテキストと新しい文字を組み合わせて幅を計算します。テキストの幅がディスプレイの幅を超えると、新しい行を開始します。また、テキストの行がディスプレイの高さを超えた場合には、最上行を削除してスクロールさせる処理を行います。このスクロール処理により、ディスプレイ上では常に最新のテキストが表示され続けます。

関数内の最終行である「time.sleep(0.04)」の数値を変更することにより、文字の表示スピードを調整可能です。

時計を作ろう

これまでに紹介したテクニックを応用して、時計を作ってみましょう。

フォントファイルのダウンロード

時計の雰囲気を盛り上げるために、Seven Segmentフォントという7セグメント風のフォントを利用します。7セグメント(通称:7セグ)は、デジタル時計などでよく使われる表示装置のことで、数字などの文字を表示するために7つの発光部分(セグメント)から構成されています。

通常のフォントから7セグ風のフォントに変更することで、より時計らしい表示の作成が可能です。

フォントの比較

Seven Segmentフォントは個人利用に関しては無料で使用できますが、商用利用の場合は5ドルのライセンス料が必要です。詳細はこちらを確認してください。

出典:dafont.com

フォントファイルは上記のサイトからダウンロードできます。Raspberry Piのブラウザで「ダウンロードボタン」を押すと、ダウンロードフォルダに保存されます。以下のコマンドを実行して、ZIPファイルを展開し「/home/pi」に移動します。

unzip /home/pi/ダウンロード/seven_segment.zip -d /home/pi/

手動で移動する場合も、「/home/pi」に以下のように保存します。

/home/piに保存されたフォントファイル

「/home/pi」に正しく保存しないと、後述するコードを実行した際にフォントファイルが読み込めず、エラーが出ます。

デジタル時計のコード

以下のプログラムは、透明ディスプレイにアニメーションと時刻を表示するためのものです。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from luma.core.render import canvas
import RPi.GPIO as GPIO
from PIL import ImageFont, Image, ImageDraw
from math import sin, cos, radians
import datetime
import time

# フォントファイルのパス'
font_path = '/home/pi/Seven Segment.ttf'

# 3つの異なるサイズのフォントを読み込み
font1 = ImageFont.truetype(font_path, 10)
font2 = ImageFont.truetype(font_path, 13)
font3 = ImageFont.truetype(font_path, 25)

GPIO.setwarnings(False)

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# スケーリング係数とX軸のオフセット
scale_factor = 0.7
x_offset = 25

# 画面中心の定数
X_CENTER = 64
Y_CENTER = 32

# 三角錐の頂点座標
pyramid = ((0, -20, 0), (-20, 20, -20), (20, 20, -20), (0, 20, 20))

try:
    while True:
        for angle in range(0, 361, 10):
    
            dt_now = datetime.datetime.now() # 現在の日時を取得
            date1 = dt_now.strftime('%m/%d') # 現在の月と日を '月/日' の形式で取得
            weekday = dt_now.strftime('%a').upper() # 現在の曜日を取得し、大文字に変換
            time1 = dt_now.strftime('%H:%M') # 現在の時刻を '時:分' の形式で取得
            second = dt_now.strftime('%S') # 現在の秒を取得
            
            f = [[0.0 for _ in range(3)] for _ in range(4)]
            r = radians(angle)

            for i in range(4):
                x1 = pyramid[i][2] * sin(r) + pyramid[i][0] * cos(r)
                ya = pyramid[i][1]
                z1 = pyramid[i][2] * cos(r) - pyramid[i][0] * sin(r)

                y2 = ya * cos(r) - z1 * sin(r)
                z2 = ya * sin(r) + z1 * cos(r)

                x3 = x1 * cos(r) - y2 * sin(r)
                y3 = x1 * sin(r) + y2 * cos(r)

                x3 = (x1 * cos(r) - y2 * sin(r)) * scale_factor + x_offset
                y3 = (x1 * sin(r) + y2 * cos(r)) * scale_factor + Y_CENTER

                f[i][0] = x3
                f[i][1] = y3
                
            with canvas(device) as draw:
                for i in range(3):
                    draw.line((f[0][0], f[0][1], f[i + 1][0], f[i + 1][1]), fill="white")
                    draw.line((f[i + 1][0], f[i + 1][1], f[(i + 1) % 3 + 1][0], f[(i + 1) % 3 + 1][1]), fill="white")
                # テキストを描画します
                draw.text((60,5), date1, font = font2, fill = "white")
                draw.text((108,5), weekday, font = font1, fill = "white")
                draw.text((56, 24), time1, font = font3, fill= "white")
                draw.text((114,33), second, font = font2, fill = "white")
                                
            time.sleep(0.1)

except KeyboardInterrupt:
    # キーボード割り込み(Ctrl+Cなど)が発生した場合に実行される
    GPIO.cleanup() # 使用していたGPIOピンをクリーンアップして、デフォルトの状態(未使用状態)に戻す

無限ループ内で現在の時刻を取得し、三角錐のアニメーションとともに時刻を表示しています。アニメーションはcanvasコンテキストを使用してディスプレイに描画され、時刻や曜日、秒数も同時に表示されます。

アナログ時計のコード

以下のコードは透明ディスプレイ上にアナログ時計を表示するものです。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from luma.core.render import canvas
from PIL import ImageFont
import RPi.GPIO as GPIO
from math import sin, cos, pi
import time
import datetime

GPIO.setwarnings(False)

# フォントファイルのパス
font_path = '/home/pi/Seven Segment.ttf'

# フォントを読み込み
font1 = ImageFont.truetype(font_path, 13)
font2 = ImageFont.truetype(font_path, 14)

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# 画面のサイズと中心の定数
width = 128
height = 64
center_x = width // 2
center_y = height // 2

def draw_hand(draw, center_x, center_y, x, y, thickness, fill):
    # 時計の針を指定した太さで描画する関数
    if thickness == 1:
        draw.line((center_x, center_y, x, y), fill=fill)
    else:
        offset = thickness // 2
        for i in range(-offset, offset + 1):
            draw.line((center_x, center_y + i, x, y + i), fill=fill)
            if i != 0:  # 中心の線を除く水平方向の補強
                draw.line((center_x + i, center_y, x + i, y), fill=fill)

try:
    while True:
        with canvas(device) as draw:
            now = datetime.datetime.now()
            hours, minutes, seconds = now.hour, now.minute, now.second
            day = now.day
            weekday = now.strftime("%a").upper()  # 曜日を英語の略称(大文字)で取得

            # 外側の長方形を描画
            draw.rectangle((0, 0, width - 1, height - 1), outline="white")

            # 時間の目盛りを描画
            for i in range(12):
                angle = pi * (2 * i / 12 - 0.5)
                mark_inner_x = center_x + int((width // 2 - 2) * 0.85 * cos(angle))
                mark_inner_y = center_y + int((height // 2 - 2) * 0.85 * sin(angle))
                mark_outer_x = center_x + int((width // 2 - 2) * cos(angle))
                mark_outer_y = center_y + int((height // 2 - 2) * sin(angle))
                draw.line((mark_inner_x, mark_inner_y, mark_outer_x, mark_outer_y), fill="white")

            # 時針を描画
            hour_angle = pi * (2 * hours / 12 - 0.5)
            hour_x = center_x + int((width // 2 - 2) * 0.5 * cos(hour_angle))
            hour_y = center_y + int((height // 2 - 2) * 0.5 * sin(hour_angle))
            draw_hand(draw, center_x, center_y, hour_x, hour_y, 3, "white")

            # 分針を描画
            minute_angle = pi * (2 * minutes / 60 - 0.5)
            minute_x = center_x + int((width // 2 - 2) * 0.8 * cos(minute_angle))
            minute_y = center_y + int((height // 2 - 2) * 0.8 * sin(minute_angle))
            draw_hand(draw, center_x, center_y, minute_x, minute_y, 2, "white")

            # 秒針を描画
            second_angle = pi * (2 * seconds / 60 - 0.5)
            second_x = center_x + int((width // 2 - 2) * 0.9 * cos(second_angle))
            second_y = center_y + int((height // 2 - 2) * 0.9 * sin(second_angle))
            draw.line((center_x, center_y, second_x, second_y), fill="white")

            # 曜日と日付のテキストを表示
            draw.text((center_x - 34, center_y - 7), weekday, font=font1, fill="white")
            draw.text((center_x + 18, center_y - 7), f"{day:02d}", font=font2, fill="white")

        time.sleep(1)  # 1秒ごとに画面を更新

except KeyboardInterrupt:
    GPIO.cleanup()  # 使用していたGPIOピンをクリーンアップ


draw_hand関数は、時計の針を描画するための関数で、時針、分針の針の太さに応じて線を太く描くことができます。

プログラムは無限ループ内で動作し、毎秒ごとに時計を更新します。まず、外側の長方形を描画して枠を設定し、次に時刻に応じた12の目盛りを画面に表示します。その後、現在時刻から時針、分針、秒針の位置を計算し、それぞれの針を適切な角度で描画します。

加えて、現在の曜日と日付もディスプレイに表示されます。曜日は英語の略称で、日付は2桁の数字で表示されます。画面の更新は1秒ごとに行われ、時計がリアルタイムで動作するようになっています。

画像を表示してみよう

透明ディスプレイに画像を表示する方法を紹介します。ここでは、僕が作成した「tenki.png」という画像を表示してみます。

以下のボタンを押すと、画像をダウンロードできます。

tenki.png

tenki.pngはCanvaを使って作成しました。自身で作成する場合は、64×64ピクセル程度のサイズで作成すると良いでしょう。複雑な画像は、つぶれて表示されてしまいます。シンプルで塗りつぶしの少ない、アイコンのような画像が表示に向いています。

ダウンロードした「tenki.png」をラズパイの「/home/pi」に保存します。

以下のコードは透明ディスプレイに画像とテキストを表示するためのものです。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from PIL import Image, ImageOps, ImageDraw, ImageFont
import RPi.GPIO as GPIO

# GPIOの警告を無効にします
GPIO.setwarnings(False)

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# 画像のパス
image_path = "tenki.png"

# 画像ファイルを開きます
image = Image.open(image_path)

# 画像の色を反転します
image = ImageOps.invert(image.convert('RGB'))

# OLEDディスプレイ用の背景画像を作成します(ディスプレイの解像度に合わせる)
background = Image.new('1', (device.width, device.height), "black")

# 背景画像上の表示したい座標を設定します
x, y = 0, 0  # 画像を左上に配置

# 背景画像に画像を貼り付けます
background.paste(image, (x, y))

# フォントのパスとサイズ設定
font_path = "/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf"
font_size = 11
font = ImageFont.truetype(font_path, font_size)
draw = ImageDraw.Draw(background)
text = "Hello,OLED!"  # 表示したいテキスト
text_x, text_y = 60, 10  # テキストの開始座標(画像の右側)

# テキストを描画します
draw.text((text_x, text_y), text, font=font, fill="white")

# 画像をディスプレイに表示します
device.display(background.convert('1'))

最初に指定されたパスから画像ファイル「tenki.png」を読み込み、その色をRGB形式に変換した後、反転させます。次にディスプレイの解像度に合わせた新しい黒背景の画像を作成し、その背景に読み込んだ画像を左上の座標に配置します。

また、背景画像上に「Hello, OLED!」というテキストを描画します。最終的に、この背景画像をディスプレイに表示することで、画像とテキストが組み合わされた内容を表示します。

図形を表示してみよう

PIL(Pillowライブラリ)には、さまざまな図形を描画するための関数が多数用意されています。これらの関数を使えば、シンプルなコードで図形を表示できます。図形やテキストを組み合わせることにより、イラストやマークのような表示も可能になります。

そぞら
そぞら

後で紹介するニュースを表示するコードでは、「NEW」のアイコンを二つの円と一つの四角形を組み合わせて表現しています。

以下のコードはディスプレイに6種類の図形を描画するためのものです。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw
import RPi.GPIO as GPIO

GPIO.setwarnings(False)

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# 新しい画像オブジェクトを作成
image = Image.new('1', (device.width, device.height))

# 画像に描画するためのDrawオブジェクトを作成
draw = ImageDraw.Draw(image)

# 左下に四角形を描画
draw.rectangle([(10, 48), (30, 64)], fill="white")

# 右上に円を描画
draw.ellipse([(98, 2), (118, 22)], fill="white")

# 左上から右下へ線を描画
draw.line([(0, 0), (128, 64)], fill="white")

# 中央に三角形を描画
draw.polygon([(60, 25), (70, 5), (80, 25)], outline="white")

# 中央右に塗りつぶしのない四角形を描画
draw.rectangle([(50, 40), (70, 60)], outline="white")

# 左上に塗りつぶしのない円を描画
draw.ellipse([(5, 15), (25, 35)], outline="white")


# 画像をディスプレイに表示
device.display(image)

図形を描画する際の座標指定について説明します。座標は図形の位置を指定するために使用されます。具体的には、ディスプレイの左上を(0, 0)として、右方向へのX座標と下方向へのY座標で位置を定めます。

各図形の座標の指定方法は次の通りです。

  • 線・・・始点と終点の座標を指定
  • 円・・・外接する四角形の左上と右下の座標を指定
  • 三角形・・・三点の座標をリストとして指定
  • 四角形・・・左上の角と右下の角の座標を指定

ニュースを表示してみよう

以下のコードは透明ディスプレイに、yahooニュースの見出しを表示するためのものです。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from PIL import ImageFont, ImageDraw, Image
import RPi.GPIO as GPIO
from datetime import datetime, timedelta
import time
import requests
from bs4 import BeautifulSoup
import re

GPIO.setwarnings(False)

def fetch_news():
    url = 'https://news.yahoo.co.jp'
    response = requests.get(url)
    soup = BeautifulSoup(response.text, "html.parser")
    elems = soup.find_all(href=re.compile("news.yahoo.co.jp/pickup"))
    result_list = []

    for elem in elems[:6]:  # 最初の6つの要素のみ処理します
        if elem.contents:  # elem.contentsが空でないかを確認します
            result_list.append(elem.contents[0])  # elem.contents[0]をリストに追加します
        
    # 現在時刻の取得
    now = datetime.now()
    # 時刻をフォーマット
    fetched_time = now.strftime('%Y-%m-%d %H:%M:%S')   
    # 取得時刻の印刷
    print(f"ニュース取得!: {fetched_time}")        
        
    return result_list

def draw_text_with_scroll(text, font, device):
    top_margin = 19  # 上マージン 
    offset_x = 40  # テキストの開始位置を図形の右側に設定
    line_height = font_size_s + 4  # 行の高さ
    text_area_width = device.width - offset_x
    lines = []
    current_line = ""

    for char in text:
        current_line += char
        bbox = font.getbbox(current_line)
        line_width = bbox[2] - bbox[0]

        if line_width > text_area_width:
            lines.append(current_line[:-1])  # 最後の文字を除外して新しい行を開始
            current_line = char

        if (len(lines) + 1) * line_height + top_margin > device.height:
            lines.pop(0)

        draw.rectangle((offset_x, 0, device.width, device.height), fill="black")  # テキストエリアをクリア
        text_y = top_margin
        for line in lines:
            draw.text((offset_x, text_y), line, font=font, fill="white")
            text_y += line_height
        draw.text((offset_x, text_y), current_line, font=font, fill="white")

        device.display(image)
        time.sleep(0.1)  # 文字を追加する速度を調整
        
# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# フォントのパスとサイズ設定
font_path = "/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf"
font_size = 16
font_size_s = 11
font = ImageFont.truetype(font_path, font_size)
font_s = ImageFont.truetype(font_path, font_size_s)

# 新しい画像オブジェクトを作成
image = Image.new('1', (device.width, device.height))

# 画像に描画するためのDrawオブジェクトを作成
draw = ImageDraw.Draw(image)

# 画面中心の定数
X_CENTER = 64
Y_CENTER = 32
# 円の半径
radius = 9

# 塗りつぶしの円を描画
draw.ellipse((0, Y_CENTER - radius, X_CENTER - 50 + radius, Y_CENTER + radius), fill="white")
draw.ellipse((X_CENTER - radius - 39, Y_CENTER - radius, X_CENTER - 39 + radius, Y_CENTER + radius), fill="white")
# 塗りつぶしの四角形を描画
draw.rectangle([(16,23),(22,41)], fill="white")

# 太字テキストを描画するために同じテキストを重ねて描画
offsets = [(0, 0), (0, 1)]  # 縦に1ピクセルズラす
for x, y in offsets:
    draw.text((x + 5, y + 24), "NEW", font=font, fill=0)

# 画面に表示
device.display(image)

# 最初のニュース取得
news_list = fetch_news()
# 最後にニュースを更新した時刻
last_updated = datetime.now()

while True:
    current_time = datetime.now()
    # 10分ごとにニュースを更新
    if current_time - last_updated >= timedelta(minutes=10):
        news_list = fetch_news()
        last_updated = current_time

    # ニュースリストを順に表示
    for news in news_list:
        draw_text_with_scroll(news, font_s, device)
        time.sleep(1.5)  # 各ニュース間の待ち時間

fetch_news関数はYahooニュースの主要トピックスの見出しを取得して、そのテキストをリストに格納します。このリストはニュースをディスプレイに表示するために使用されます。

チェックポイント

プログラムを使用してウェブサイトから情報を自動的に抽出するプロセスをスクレイピングと呼びます。スクレイピングを行う際にはいくつかの注意点があります。まず、対象ウェブサイトの利用規約を確認し、スクレイピングが許可されているかどうかを確かめましょう。また、スクレイピングによってウェブサイトに過大な負荷をかけることは避けるべきです。データの抽出間隔を適切に設定して、サーバーに対する影響を最小限に抑える必要があります。

raw_text_with_scroll関数では、特定の位置からテキストのスクロールを開始する設定を行っています。offset_x変数はテキストの開始位置を設定しています。これにより、図形が描画された部分の右側にテキストが表示されます。テキストはディスプレイの幅に応じてスクロールし、必要に応じて改行されます。

テキストの左側に表示される「NEW」のアイコン表示は、二つの塗りつぶしの円と一つの四角形を描画し、その中に太字風のテキストを表示しています。テキストを一度描いた後、同じテキストを1ピクセル下にずらして重ねて描くことで、太字のように見せています。

無限ループ内では10分ごとにニュースを更新し続けます。各ニュース項目は一定の間隔でスクロール表示され、新しいニュースが取得されると表示が更新されます。

Yahooニュースの主要トピックスを取得する方法は【Python】スクレイピングの方法と注意事項についてという記事を参考にさせていただきました。

ビットコインのチャートを表示してみよう

以下のコードは、ビットコインの価格変動を透明ディスプレイにグラフとして表示するものです。

from luma.core.interface.serial import spi
from luma.oled.device import ssd1306
from luma.core.render import canvas
from PIL import ImageFont
import RPi.GPIO as GPIO
import requests
import json
from datetime import datetime
import time

GPIO.setwarnings(False)

def draw_dotted_line(draw, start, end, interval=3, dot_length=1):
    """点線を描画する関数。startからendまで、指定された間隔で点線を描画します。"""
    x1, y1 = start
    x2, y2 = end
    total_length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
    num_dots = int(total_length / interval)

    for i in range(num_dots):
        dot_start = i * interval + (interval - dot_length) / 2
        dot_end = dot_start + dot_length
        dot_start_x = x1 + (x2 - x1) * (dot_start / total_length)
        dot_start_y = y1 + (y2 - y1) * (dot_start / total_length)
        dot_end_x = x1 + (x2 - x1) * (dot_end / total_length)
        dot_end_y = y1 + (y2 - y1) * (dot_end / total_length)
        draw.line([(dot_start_x, dot_start_y), (dot_end_x, dot_end_y)], fill="white")

# SPI接続の設定
serial = spi(device=0, port=0, gpio_DC=25, gpio_RST=27)
device = ssd1306(serial)

# CoinGeckoのAPI URL
url = 'https://api.coingecko.com/api/v3/coins/bitcoin/market_chart'

# APIリクエストのパラメータ
params = {
    'vs_currency': 'jpy',  # 価格情報の通貨単位を日本円(JPY)に設定
    'days': 80  # 過去80日間の価格データを取得する
}

# フォントのパスとサイズ設定
font_path = "/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf"
font = ImageFont.truetype(font_path, 11)
font_s = ImageFont.truetype(font_path, 8)

try:
    while True:
        # 価格データを取得
        response = requests.get(url, params=params)
        response.raise_for_status() # エラーがあれば例外を発生させる
        data = json.loads(response.text) # 取得したデータをJSON形式からPythonの辞書に変換
        prices = [x[1] for x in data['prices']] # 'prices' キーに含まれる価格データのみを抽出

        min_price, max_price = min(prices), max(prices) # 最小価格と最大価格を計算

        # 価格のリストを並べかえ
        sorted_prices = sorted(prices)
        prices_len = len(sorted_prices)

        # 価格の中央値を計算
        if prices_len % 2 == 1:
            # リストの長さが奇数の場合、中央の値が中央値
            median_price = sorted_prices[prices_len // 2]
        else:
            # リストの長さが偶数の場合、中央の2つの値の平均が中央値
            middle1 = sorted_prices[(prices_len // 2) - 1]
            middle2 = sorted_prices[prices_len // 2]
            median_price = (middle1 + middle2) / 2

        # 最大値、中央値、最小値をフォーマット
        formatted_min_price = f"{min_price / 1_000_000:.1f}M"
        formatted_median_price = f"{median_price / 1_000_000:.1f}M"
        formatted_max_price = f"{max_price / 1_000_000:.1f}M"

        last_price = prices[-1] # 最後の価格(最新の価格)
        formatted_price = "{:,}".format(int(last_price)) # 3桁ごとにカンマを入れる

        # 画面サイズ
        width = device.width
        height = device.height

        # グラフのマージン設定
        top_margin = 13
        bottom_margin = 0
        left_margin = 22
        right_margin = 0

        # グラフの内側マージン(グラフと枠の間の隙間)
        inner_margin = 3  # ここで内側のマージンの値を調整

        # グラフ描画エリアの計算(外側の枠のサイズを固定)
        graph_width = width - left_margin - right_margin
        graph_height = height - top_margin - bottom_margin

        # グラフ描画
        with canvas(device) as draw:
            # 最新のビットコイン価格を画面上部に表示
            draw.text((28, 0), f"BTC:{formatted_price}円", font=font, fill="white")
            # 最大値、中央値、最小値を表示(数値軸の目盛)
            draw.text((0, 10), formatted_max_price, font=font_s, fill="white")
            draw.text((0, 33), formatted_median_price, font=font_s, fill="white")
            draw.text((0, 55), formatted_min_price, font=font_s, fill="white")

            # 価格データを線グラフとして描画(内側マージンを考慮して座標を計算)
            for i in range(1, len(prices)):
                x1 = left_margin + inner_margin + (i - 1) * ((graph_width - inner_margin * 2) / (len(prices) - 1))
                y1 = top_margin + inner_margin + (graph_height - inner_margin * 2) - ((prices[i - 1] - min_price) / (max_price - min_price) * (graph_height - inner_margin * 2))
                x2 = left_margin + inner_margin + i * ((graph_width - inner_margin * 2) / (len(prices) - 1))
                y2 = top_margin + inner_margin + (graph_height - inner_margin * 2) - ((prices[i] - min_price) / (max_price - min_price) * (graph_height - inner_margin * 2))

                # 線を描画
                draw.line([(x1, y1), (x2, y2)], fill="white")

            # グラフの外側の枠を描画(外側の枠のサイズを固定)
            draw.rectangle([left_margin, top_margin, left_margin + graph_width - 1, top_margin + graph_height - 1], outline="white")
            
            # 横方向の中央に点線を引く
            middle_y = top_margin + graph_height / 2
            draw_dotted_line(draw, (left_margin, middle_y), (width - right_margin - 1, middle_y))

            # 縦方向に2本の点線を引く
            third_x1 = left_margin + graph_width / 3
            third_x2 = left_margin + 2 * graph_width / 3
            draw_dotted_line(draw, (third_x1, top_margin), (third_x1, height - bottom_margin - 1))
            draw_dotted_line(draw, (third_x2, top_margin), (third_x2, height - bottom_margin - 1))            

        # 現在時刻の表示
        now = datetime.now()
        fetched_time = now.strftime('%Y-%m-%d %H:%M:%S')
        print(f"取得時刻: {fetched_time}")

        # 10分間プログラムの実行を停止する
        time.sleep(600)

except IOError as e:
    print(e)

except KeyboardInterrupt:
    # キーボード割り込み(Ctrl+Cなど)が発生した場合に実行される
    GPIO.cleanup() # 使用していたGPIOピンをクリーンアップして、デフォルトの状態(未使用状態)に戻す

まず、CoinGeckoのAPIから過去80日間のビットコイン価格データを日本円単位で取得します。データを受け取った後、最小価格、最大価格、中央値を計算し、軸の目盛の数値として表示する準備をします。さらに、最新の価格を整形してグラフ上部に表示します。

グラフの表示エリアは、ディスプレイのサイズに基づいて計算され、適切なマージンを設定して内部にグラフを描画します。価格データは線グラフとして描かれ、各点は連続して接続されます。グラフの外側には枠が描かれ、縦横の中間点に点線が引かれます。

time.sleep(600)は、プログラムがデータを取得してから次のデータ取得まで10分間待つように設定しています。この待機時間を極端に短くすると、CoinGeckoのAPIサーバーへのアクセスが頻繁になりすぎてしまいます。自動で情報収集する場合は、相手側のサーバーに過度な負荷をかけないよう注意が必要です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です