卓上ロボット制作記録②
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+に変えました。
結果的に余計なケーブルが不要になったので、スッキリとした構成になりました。

その内容を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-busdevice2. GC9D01ライブラリのダウンロード
GitHubからMarc4tさんのドライバコードをダウンロードします。
cd ~
git clone https://github.com/Marc4t/GC9D01.git
cd GC9D013. テストコード作成
ダウンロードしたフォルダの中に、以下の 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 = False4. 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()成功するとこんな感じで表示されます。

立体的でとてもキレイに表示されます🥰
液晶ディスプレイへの表示で精魂尽きてしまったので、
続きは日を改めます…💦
それまでに外装用のぬいぐるみも探しておかないと…
当初はこのぬいぐるみを使おうと思いましたが、目が小さかったのでまた別のを探してみます。

