見出し画像

卓上ロボット制作記録②

LLMと連動した卓上ロボットの制作記録です。
時間が取れた時にゆっくりと進めています。


前回から更にコンパクト化をするため、
Raspberry Pi5 → Pi3A+に変えました。
基本的にクローゼットから掘り起こしたパーツを再利用してます。

Raspberry Pi5の性能でもローカルで何か処理をさせると遅すぎるので、
耳(音声認識:ASR)、声(音声生成:TTS)、目(視覚)はすべてGPU付きのPCへ処理を移しました。

機能:👂 耳(音声入力)
・ハードウェア:
ReSpeaker Lite
・ソフトウェア:
Whisper ASR + VAD + gemma3-4B(自作APIサーバ)

機能:🗣️ 声(音声出力)
・ハードウェア:
3Wスピーカー(ReSpeaker Liteへ接続)
・ソフトウェア:
Style-Bert-VITS2(API接続)

機能:👁️ 目(視覚認識)
・ハードウェア:
Camera Module V3
・ソフトウェア:
Ollama(gemma3-4B)(API接続)

機能:😊 目(表情)
・ハードウェア:
WAVESHARE 0.71inch 丸型LCD (GC9D01)
・ソフトウェア:
 GC9D01ディスプレイドライバ(Marc4t氏のCircuitPython_GC9D01ライブラリ)

今回使ったハードウェア

収納スペースを考えると Raspberry Pi Zero2 Wでもよかったんだけど、
ReSpeaker Lite への電力不足で別途給電が必要だったので、
途中でUSB-A端子を持っている Raspberry Pi 3A+に変えました。
結果的に余計なケーブルが不要になったので、スッキリとした構成になりました。

カメラ映像をollama経由のgemma3:4Bで画像認識し、
その内容をTTS経由でメッセージとして発生します。その間、約2秒

電源は、交換済みのロボット掃除機のバッテリーを使いました。
バッテリーからのUSB経由での給電にあたり、重宝したのがこのケーブルです。

ヒューズ付きで、USB2本へ給電できます。
(Raspberry Pi とスピーカーへ給電します)


またもや鬼門だったSPIディスプレイ接続

今回は外装を「ゴミ箱」→「ぬいぐるみ」に変えようと思い、
「目」を表示させるための液晶ディスプレイとして、
この丸形のもの左右分の2つを新規に調達しました。

SPI接続だから、前回作ったコードを元にベースにGC9D01用にコードを作り直すだけと思っていたのですが、今回も全く何も映らず…

Claude と Geminiに相談しながら数時間…
いろいろ試しつつも全く表示できません…💦
(サンプルコードは ESP32/Arduino 用のものしか提供されていない)

何か手があるはずと思い、コントローラーの GC9D01 で検索したところ、
以下の Raspberry Pi用のライブラリを発見!!

見事に問題は解決し、瞳用の画像を表示できました!🎉
Marc4tさん、公開を感謝します!!


GC9D01で画像を表示するまで

もし、Raspberry Pi で GC9D01 を使って液晶画面に画像を表示したい方がいらっしゃいましたら、以下の手順を参考にしてみてください

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

Raspberry PiのGPIOをCircuitPython形式で扱えるようにするためのライブラリをインストールします。

# システムの更新(念のため)
sudo apt update

# pipのアップグレード
python3 -m pip install --upgrade pip

# CircuitPython用ライブラリのインストール
pip3 install --upgrade adafruit-blinka adafruit-circuitpython-busdevice

2. GC9D01ライブラリのダウンロード

GitHubからMarc4tさんのドライバコードをダウンロードします。

cd ~
git clone https://github.com/Marc4t/GC9D01.git
cd GC9D01

3. テストコード作成

ダウンロードしたフォルダの中に、以下の python コードをコピーしてテストコードのファイルを作成します。

import time
import board
import busio
import digitalio
from gc9d01_library import GC9D01

# --- ピン設定 (あなたの配線: BCM番号) ---
# Blinkaでは "board.Dxx" でGPIOを指定します
pin_dc  = board.D25
pin_rst = board.D27
pin_cs_left = board.D24
pin_cs_right = board.D23
pin_bl  = board.D18

# --- バックライトの点灯 (手動制御) ---
# ライブラリ自体にはBL制御がない場合があるので、ここで点けます
bl = digitalio.DigitalInOut(pin_bl)
bl.direction = digitalio.Direction.OUTPUT
bl.value = True  # ON

# --- SPIバスの準備 ---
spi = busio.SPI(clock=board.SCK, MOSI=board.MOSI)

# --- ディスプレイ接続 (まずは左目でテスト) ---
print("Initializing Left Display...")

# ピン定義の変換
dc_pin = digitalio.DigitalInOut(pin_dc)
cs_pin = digitalio.DigitalInOut(pin_cs_left)
rst_pin = digitalio.DigitalInOut(pin_rst)

# ドライバの初期化
# ここでMarc4t氏の初期化シーケンスが走ります
display = GC9D01(spi, dc_pin, cs_pin, rst_pin)

print("Display initialized!")

# --- 描画テスト ---
# 画面を赤、緑、青で塗りつぶしループ
colors = [0xF800, 0x07E0, 0x001F, 0xFFFF] # 赤, 緑, 青, 白

try:
    while True:
        for color in colors:
            print(f"Filling color: {hex(color)}")
            display.fill_screen(color)
            time.sleep(1)

except KeyboardInterrupt:
    print("\nFinished.")
    # 終了時にバックライトを消す
    bl.value = False

4. LCD と Raspberry Pi の結線

VCC(電源):Pin 1(または Pin 17)(3.3V)
GND(接地):Pin 6(または Pin 9, 14, 20) (GND)
DIN(データ:MOSI):Pin 19(GPIO 10)
CLK(クロック:SCLK):Pin 23(GPIO 11)
DC(データ/コマンド):Pin 22(GPIO 25)
RCT(リセット):Pin 13(GPIO 27)
BL(バックライト):Pin 12(GPIO 18)
CS(左目):Pin 18(GPIO 24)
CS(右目):Pin 16(GPIO 23)


テストコードを実行し、画面上に次々と色を変えて表示すると成功です。


5. ビットマップ表示のサンプルコード

画像ファイル「eye.png(160x160ピクセル)」を表示させるためのサンプルコードを載せておきます。
(コードと画像ファイルは同じフォルダに置いてください)

先ほどのテストコードではゆっくりと画面が切り替わっていますが、
このコードでは一瞬で表示されます。

import time
import sys
import os
import board
import busio
import digitalio
import spidev
from PIL import Image
from gc9d01_library import GC9D01

# --- 設定 ---
IMAGE_FILE = "eye.png"  # 表示したい画像ファイル名
WIDTH = 160
HEIGHT = 160
SPI_SPEED = 40000000    # 40MHz

# --- ピン設定 (BCM番号) ---
PIN_DC  = board.D25
PIN_RST = board.D27
PIN_BL  = board.D18
PIN_CS_L = board.D24    # 左目
PIN_CS_R = board.D23    # 右目

# --- グローバル変数 ---
spi_raw = None
dc_pin = None
cs_l = None
cs_r = None

def init_display():
    """初期化シーケンス (ライブラリ使用)"""
    global spi_raw, dc_pin, cs_l, cs_r
    
    print("1. システム初期化中...")
    
    # バックライトON
    bl = digitalio.DigitalInOut(PIN_BL)
    bl.direction = digitalio.Direction.OUTPUT
    bl.value = True
    
    # ピン定義
    dc_pin = digitalio.DigitalInOut(PIN_DC)
    dc_pin.direction = digitalio.Direction.OUTPUT
    
    cs_l = digitalio.DigitalInOut(PIN_CS_L)
    cs_l.direction = digitalio.Direction.OUTPUT
    cs_l.value = True # High (非選択)
    
    cs_r = digitalio.DigitalInOut(PIN_CS_R)
    cs_r.direction = digitalio.Direction.OUTPUT
    cs_r.value = True # High (非選択)
    
    rst_pin = digitalio.DigitalInOut(PIN_RST)

    # --- ライブラリによるWakeup ---
    # ライブラリには「ダミーのCSピン」を渡して、実際のCS制御は邪魔させない
    print("2. ディスプレイ起動シーケンス...")
    temp_spi = busio.SPI(clock=board.SCK, MOSI=board.MOSI)
    
    # 左右それぞれを初期化するために、手動でCSを下げてライブラリを騙す
    
    # 左目初期化
    cs_l.value = False
    display_l = GC9D01(temp_spi, dc_pin, digitalio.DigitalInOut(board.D5), rst_pin)
    cs_l.value = True
    
    # 右目初期化 (RSTは共有なので、リセットなしで初期化コマンドだけ送る必要があるが
    # ライブラリの仕様上、もう一度newするとRSTされてしまう。
    # しかしGC9D01はRSTピン共有なら同時にリセットされるので、
    # 左目の初期化時点ですでに右目もハードリセットは終わっている。)
    
    # ここでは「両方のCSを下げて」もう一度初期化コマンドを送ることで同期をとる
    cs_l.value = False
    cs_r.value = False
    display_dual = GC9D01(temp_spi, dc_pin, digitalio.DigitalInOut(board.D6), rst_pin)
    cs_l.value = True
    cs_r.value = True
    
    temp_spi.deinit() # ライブラリ用SPIを閉じる
    
    print("3. 高速SPIモードへ切り替え...")
    spi_raw = spidev.SpiDev()
    spi_raw.open(0, 0)
    spi_raw.max_speed_hz = SPI_SPEED
    spi_raw.mode = 0
    spi_raw.no_cs = True

def image_to_data(img):
    """PIL画像をRGB565データ列に変換"""
    img = img.resize((WIDTH, HEIGHT)).convert('RGB')
    pixels = list(img.getdata())
    buffer = bytearray(WIDTH * HEIGHT * 2)
    
    idx = 0
    for r, g, b in pixels:
        # RGB565変換
        val = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
        buffer[idx] = (val >> 8) & 0xFF
        buffer[idx+1] = val & 0xFF
        idx += 2
    return buffer

def show_data(data, left=True, right=True):
    """バイナリデータを表示"""
    # CS操作
    if left: cs_l.value = False
    if right: cs_r.value = False
    
    try:
        # 範囲設定 (0,0) - (159,159)
        dc_pin.value = False # Command
        spi_raw.writebytes([0x2A]) # Column
        dc_pin.value = True
        spi_raw.writebytes([0, 0, 0, 159])
        
        dc_pin.value = False
        spi_raw.writebytes([0x2B]) # Row
        dc_pin.value = True
        spi_raw.writebytes([0, 0, 0, 159])
        
        # 書き込み開始
        dc_pin.value = False
        spi_raw.writebytes([0x2C])
        
        # データ送信
        dc_pin.value = True # Data
        chunk = 4096
        for i in range(0, len(data), chunk):
            spi_raw.writebytes(data[i:i+chunk])
            
    finally:
        cs_l.value = True
        cs_r.value = True

def main():
    try:
        init_display()
        
        # 画像読み込み
        target_image = IMAGE_FILE
        if len(sys.argv) > 1:
            target_image = sys.argv[1]
            
        if os.path.exists(target_image):
            print(f"画像読み込み: {target_image}")
            img = Image.open(target_image)
            img_data = image_to_data(img)
        else:
            print(f"画像({target_image})が見つかりません。テストパターンを表示します。")
            # 赤色データを作成
            img_data = bytearray([0xF8, 0x00] * 160 * 160)

        print("表示開始!")
        
        while True:
            # 両目に画像を表示
            show_data(img_data, left=True, right=True)
            
            # チラつき防止のため少し待つ(静止画ならこれで十分)
            time.sleep(1)

    except KeyboardInterrupt:
        print("\n終了します")
        spi_raw.close()

if __name__ == "__main__":
    main()

成功するとこんな感じで表示されます。

液晶ディスプレイに凸レンズがついているので、
立体的でとてもキレイに表示されます🥰

液晶ディスプレイへの表示で精魂尽きてしまったので、
続きは日を改めます…💦

それまでに外装用のぬいぐるみも探しておかないと…
当初はこのぬいぐるみを使おうと思いましたが、目が小さかったのでまた別のを探してみます。

いいなと思ったら応援しよう!