0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry Pi Zero 2 W でなんちゃってスマートグラス

Posted at

Raspberry Pi Zero 2 W を「電源入れるだけで動く」スマートグラス表示サーバにした

(Wi-Fi自動接続 / HTML送信 / ST7735表示 / systemd自動起動)

スマートグラス側(Raspberry Pi Zero 2 W)をモバイルバッテリーに繋いで電源ONすると、

  1. 勝手にWi-Fiに繋がる
  2. 勝手にHTMLサーバ(Flask)が立ち上がる
  3. スマホ/PCのブラウザで http://<PiのIP>:8000/ を開く
  4. 文字を送信すると、メガネ側ディスプレイに即表示

までが ノー操作 で完了する構成です。

参考にしたもの

メガネに「ディスプレイ+反射鏡+レンズ+透明板(コンバイナ)」を収める光学レイアウトの発想を参考にしました。
本記事は、表示制御を Raspberry Pi Zero 2 W + Web送信(Flask) に置き換え、さらに “電源入れるだけ運用” まで詰めた版です。

仕組み(超ざっくり)

  • Wi-Fi自動接続:Raspberry Pi OSの標準機能(事前にSSID/パスを登録)
  • Webサーバ:Flask(フォーム → POST受信)
  • 描画:Pillowで文字を画像化
  • 表示:ST7735へSPIで転送
  • 電源ONで自動起動:systemdサービスに登録

材料

メカ(ほぼ紙工作)

  • 厚紙(筐体、遮光の壁)
  • 黒テープ(遮光と補強)
  • 両面テープ / ホットボンド(固定)

メカ

光学(代用品で成立させる)

  • プレパラート(スライドガラス)
  • 100円ショップの老眼鏡(レンズ代用:拡大&ピント調整)
  • メイク用品の鏡(反射鏡として使用)

電子

  • Raspberry Pi Zero 2 W
  • ST7735系 小型SPIディスプレイ(例:128×160)
  • モバイルバッテリー(5V)
  • 配線(SPI)

電子


配線(ST7735 / SPI)

※SPIの信号名は SCLK(SCK) / MOSI(DIN) 表記が一般的です。
(I2CのSCL/SDAと混同しやすいので、ここではSPI表記で統一します)

  • SCLK(SCK) → GPIO11(物理 23)
  • MOSI(DIN) → GPIO10(物理 19)
  • CS → GPIO8(物理 24, CE0)
  • DC → GPIO24(物理 18)
  • RST → GPIO25(物理 22)
  • VDD → 3.3V(物理 1)
  • BKL → 3.3V(直結で点灯:後述の注意あり)
  • GND → GND(物理 6)

全体構造(ざっくり)

光の流れはこの順番にすると調整がラクでした。

ディスプレイ →(反射鏡で折る)→ 老眼鏡レンズプレパラート(透明板) → 目
image.png
*参考図

  • 反射鏡:光路を折って、厚紙箱の中で距離を稼ぐ
  • 老眼鏡レンズ:表示を見やすい距離に“持ってくる”(ピント合わせ担当)
  • プレパラート:外界は透過、HUD像は反射で目に入る(簡易コンバイナ)

完成までの流れ(手順)

1) 厚紙で「光学箱」を作る(ここが勝敗)

  • メガネの片側に載るサイズで、厚紙で箱を作る
  • 内側は黒テープで徹底的に遮光
    • ここが甘いと、表示が薄くなる / 白っぽくなる / 見づらい

コツ:いきなり本固定しないで、最初はマステで“動く仮固定”が吉。

2) メイク鏡を反射鏡にする(光路を折る)

  • 鏡部分を取り出して小片にする(※私はこの方法)
  • 光学箱の中に 45°くらいで仮固定
    • ディスプレイの光が「横→前」へ折れて、箱の中で距離を稼げます

3) ディスプレイを固定(まず“点く”が最優先)

  • ST7735ディスプレイを箱の奥に設置
  • ここも仮固定でOK(あとで必ず位置調整する)

4) 老眼鏡レンズを入れる(ピントが合う場所を探す)

  • 100均老眼鏡をバラして、片側レンズだけ使う
  • 反射鏡の先(目側)に入れて、前後に動かしてピントが合う位置を探す

メモ:度数で性格が変わります。
強いほど近距離で合うが歪みやすい / 弱いほど距離が必要。

5) プレパラートを“透明板”として配置(外界とHUDの合成)

  • 目の前に来る位置にプレパラートを置く
  • 角度を少しずつ変えると、外の景色と表示のバランスが変わる

調整ポイント

  • 表示が薄い → 角度 / 遮光 / ディスプレイ輝度を見直す
  • 二重に見える(ゴースト) → プレパラート角度を微調整(表裏反射が出やすい)

6) 光学が決まったら固定して“完成形”へ

  • 位置が決まった部品から順に本固定
  • 最後に厚紙箱の隙間を黒テープで埋め、遮光を仕上げる

Raspberry Pi Zero 2 W 側(電源入れるだけ運用)

7) Wi-Fiを自動接続にしておく(最初の一回だけ)

Raspberry Pi OSでSSID/パスを登録しておけば、次回以降は電源ONで勝手に繋がります。
(GUIでも、ヘッドレスなら設定ファイルでもOK)

8) SPIを有効化(忘れがち)

Raspberry PiでSPIを使うので、有効化しておきます。

  • sudo raspi-config → Interface Options → SPI → Enable
  • 再起動:sudo reboot

9) ソフト:インストール(日本語フォント込み)

sudo apt update
sudo apt install -y fonts-noto-cjk python3-venv

# 仮想環境(例)
python3 -m venv ~/st7735-venv
source ~/st7735-venv/bin/activate

pip install --upgrade pip
pip install flask pillow st7735

注意(ライブラリ差)
ST7735系は複数のPythonライブラリがあり、APIが微妙に違います。
この記事のコードは from st7735 import ST7735 が通る st7735 パッケージを前提にしています。もしimportエラーになる場合は、利用しているディスプレイ基板に合うライブラリ(Pimoroni系など)へ置き換えてください。

10) HTMLサーバ(Flask)で「文字を受けて表示」

やっていることはシンプルにこれだけです。

受信 → 画像に描画 → ST7735へ表示
image.png

Flaskサーバコード全文(ST7735 128×160 / 日本語 / HTMLフォーム)

/home/pi/smartglass_server.py として保存します(中央寄せ表示)。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from flask import Flask, request
from PIL import Image, ImageDraw, ImageFont
from st7735 import ST7735

"""
Raspberry Pi Zero 2 W + ST7735(128x160)
ブラウザ(HTML)から文字を送って表示する

配線前提(SPI)
SCLK(SCK) -> GPIO11 (物理23)
MOSI(DIN) -> GPIO10 (物理19)
CS        -> GPIO8  (物理24, CE0)
DC        -> GPIO24 (物理18)
RST       -> GPIO25 (物理22)
VDD       -> 3.3V
BKL       -> 3.3V 直結(基板によっては注意)
GND       -> GND
"""

disp = ST7735(
    port=0,           # SPI0
    cs=0,             # CE0 (GPIO8)
    dc=24,            # GPIO24
    rst=25,           # GPIO25
    backlight=None,   # BKLは直結
    width=128,
    height=160,
    rotation=0,       # 0/90/180/270
    invert=False,
    spi_speed_hz=10_000_000,
    bgr=False,
)

# ライブラリによって begin() が必要/不要な場合があるため吸収
if hasattr(disp, "begin"):
    disp.begin()

# 日本語フォント
FONT_SIZE = 20
FONT_PATH = "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)

app = Flask(__name__)

HTML = """
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Smart Glass Control</title>
    <style>
      body { font-family: sans-serif; padding: 1rem; }
      h1   { font-size: 1.4rem; }
      input[type=text] { width: 80%; font-size: 1.2rem; padding: 0.2rem; }
      button { font-size: 1.1rem; padding: 0.3rem 1rem; margin-top: 0.5rem; }
      form { margin-bottom: 1rem; }
      .note { color: #666; font-size: 0.9rem; }
    </style>
  </head>
  <body>
    <h1>スマートグラス表示コントロール</h1>
    <p class="note">同一Wi-Fi上の端末から送信できます(認証なし)。必要なら後述の対策を入れてください。</p>

    <form method="POST" action="/text">
      <p>
        表示したい文字:
        <input type="text" name="msg" autocomplete="off" maxlength="80">
      </p>
      <button type="submit">送信して表示</button>
    </form>

    <form method="POST" action="/clear">
      <button type="submit">画面クリア</button>
    </form>
  </body>
</html>
"""

def draw_text_on_lcd(msg: str) -> None:
    # 空文字は何もしない(好みで変更OK)
    msg = (msg or "").strip()
    if not msg:
        return

    img = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
    draw = ImageDraw.Draw(img)

    # Pillowのバージョン差吸収
    if hasattr(draw, "textbbox"):
        bbox = draw.textbbox((0, 0), msg, font=font)
        text_w = bbox[2] - bbox[0]
        text_h = bbox[3] - bbox[1]
    else:
        text_w, text_h = draw.textsize(msg, font=font)

    x = (disp.width - text_w) // 2
    y = (disp.height - text_h) // 2
    draw.text((x, y), msg, fill=(255, 255, 255), font=font)

    disp.display(img)

def clear_lcd() -> None:
    img = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
    disp.display(img)

@app.route("/", methods=["GET"])
def index():
    return HTML

@app.route("/text", methods=["POST"])
def set_text():
    msg = request.form.get("msg", "")
    draw_text_on_lcd(msg)
    return HTML

@app.route("/clear", methods=["POST"])
def clear():
    clear_lcd()
    return HTML

if __name__ == "__main__":
    # 同一LANから見えるよう 0.0.0.0
    app.run(host="0.0.0.0", port=8000)

11) 「電源入れるだけ」にする:systemd自動起動

OS起動と同時にサーバを上げます。

A) いちばん安定(venv内pythonを直指定)

/etc/systemd/system/smartglass.service

[Unit]
Description=Smart Glass Flask Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi
Environment=PYTHONUNBUFFERED=1
ExecStart=/home/pi/st7735-venv/bin/python /home/pi/smartglass_server.py
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

反映:

sudo systemctl daemon-reload
sudo systemctl enable smartglass.service
sudo systemctl restart smartglass.service
sudo systemctl status smartglass.service

補足(Wi-Fi待ちが不安定なとき)
network-online.targetでも「Wi-Fiがまだ繋がってない」ケースが出ることがあります。
その場合は環境に応じて “wait-online” を使うと安定します(例:NetworkManager環境なら NetworkManager-wait-online.service)。

B) スクリプト経由で起動したい場合(任意)

/home/pi/start_smartglass.sh

#!/bin/bash
cd /home/pi
source /home/pi/st7735-venv/bin/activate
exec python /home/pi/smartglass_server.py

実行権限を付与:

chmod +x /home/pi/start_smartglass.sh

この場合、smartglass.service の ExecStart は /home/pi/start_smartglass.sh にします。

使い方(運用)

  1. Piをモバイルバッテリーに繋いで電源ON
  2. スマホ/PCを同じWi-Fiに繋ぐ
  3. PiのIPを確認(Pi側で見るなら
hostname -I
  1. ブラウザで http://:8000/ を開く
  2. 文字を送信 → スマートグラス側に表示

小ネタ:IPを探したくない場合
環境によってはhttp://raspberrypi.local:8000/ のような mDNS で開けることがあります(名前はホスト名次第)。

ハマりポイント(先に潰す)

  • 遮光不足:表示が薄くて“何も見えない”原因の9割
  • プレパラートは反射率が低い:明るい屋外だと負けやすい(暗所でめちゃ見える)
  • 二重像(ゴースト):ガラスの表裏反射。角度調整が効く
  • 電源が弱いと不安定:Pi Zero 2 Wは電圧低下で再起動しやすい

追加:表示バリエーション(関数差し替え)

左寄せ版(関数差し替え)

def draw_text_on_lcd(msg: str) -> None:
    msg = (msg or "").strip()
    if not msg:
        return

    img = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
    draw = ImageDraw.Draw(img)

    if hasattr(draw, "textbbox"):
        bbox = draw.textbbox((0, 0), msg, font=font)
        text_h = bbox[3] - bbox[1]
    else:
        _, text_h = draw.textsize(msg, font=font)

    x = 0
    y = (disp.height - text_h) // 2
    draw.text((x, y), msg, fill=(255, 255, 255), font=font)

    disp.display(img)

45°回転表示(関数まるごと差し替え)

def draw_text_on_lcd(msg: str) -> None:
    msg = (msg or "").strip()
    if not msg:
        return

    w, h = disp.width, disp.height
    base = Image.new("RGB", (w, h), (0, 0, 0))

    canvas_w, canvas_h = w * 2, h * 2
    txt_img = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
    draw = ImageDraw.Draw(txt_img)

    if hasattr(draw, "textbbox"):
        bbox = draw.textbbox((0, 0), msg, font=font)
        text_w = bbox[2] - bbox[0]
        text_h = bbox[3] - bbox[1]
    else:
        text_w, text_h = draw.textsize(msg, font=font)

    tx = (canvas_w - text_w) // 2
    ty = (canvas_h - text_h) // 2
    draw.text((tx, ty), msg, font=font, fill=(255, 255, 255, 255))

    rotated = txt_img.rotate(45, expand=True, resample=Image.BICUBIC)

    rw, rh = rotated.size
    cx = (rw - w) // 2
    cy = (rh - h) // 2
    cropped = rotated.crop((cx, cy, cx + w, cy + h))

    base.paste(cropped.convert("RGB"), (0, 0))
    disp.display(base)

セキュリティ(最低限の注意)

この構成は 同一Wi-Fi上の誰でも送信できる可能性があります。
家庭内LANなど、用途が限定されている前提なら割り切れますが、気になる場合は対策を検討してください。

  • ルータ側で端末を隔離(ゲストWi-Fi等)
  • Pi側でファイアウォールで送信元を制限
  • Flaskに簡易トークン(パスコード)を入れる(フォームにhiddenで付ける等)

安全面(重要)

  • 鏡・プレパラート加工は破片が危険(目の近くなので特に注意)
  • 顔面に近い配線はショートと発熱を最優先で潰す
  • 歩行中・運転中の使用は避ける(注意が削れます)
  • BKLを3.3V直結は基板によって電流が大きい場合があります
    • 発熱が気になる場合は、抵抗を入れる / トランジスタで駆動する等を検討してください
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?