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?

OCuLink eGPUの電源事故を防ぐ: Raspberry Pi 4BとHome AssistantでミニPCとdGPUを安全に連動する

0
Last updated at Posted at 2026-07-26

背景

ミニPCの GMKtec NucBox K8 Plus に、OCuLink 経由で外付け dGPU を接続して使っています。以降、このミニPCを略して K8 と呼びます。

この構成で怖いのは、K8本体が起動したまま dGPU 側の電源だけが落ちる状態です。PCIe デバイスが稼働中に消えるため、OS、ドライバ、実行中のワークロードにとってかなり嫌な状態になります。

そこで Raspberry Pi 4B を電源制御のコントローラーにして、K8本体と外付けGPUドックの電源を連動させました。

今回の構成では、外付けGPUドックは AOOSTAR AG02 eGPUドックドッキングステーション、その AG02 への給電制御には Matter 対応スマートプラグの TP-Link Tapo P110M を使っています。

この記事の主題は、外付けGPUを便利にON/OFFすることではなく、K8本体とdGPU電源の不整合を検出し、安全側へ倒すことです。

全体構成

Windows 11
  |
  | ssh pi4 "cd ~/WS/k8ctl && ./k8ctl start|stop|restart"
  v
Raspberry Pi 4B
  |
  +-- k8ctl
  |     +-- Home Assistant API で Tapo P110M を ON/OFF
  |     +-- Wake-on-LAN で K8 を起動
  |     +-- SSH で K8 を shutdown
  |     +-- K8 上の /sys と /dev/dri で外付けGPU検出
  |
  +-- Docker Compose
        +-- homeassistant
        +-- matterjs-server

K8本体は Wake-on-LAN で起動し、SSH 経由で shutdown します。AG02 は Tapo P110M の ON/OFF により給電を制御します。

起動・停止の方針

起動時:

Tapo P110M ON
  -> AG02 / dGPU 側の電源が安定するまで待つ
  -> K8 を Wake-on-LAN で起動
  -> ping 応答待ち
  -> SSH 接続待ち
  -> 外付けGPUの render node 検出待ち

停止時:

K8 を SSH で shutdown
  -> ping が落ちるまで待つ
  -> shutdown 後の短い待ち
  -> Tapo P110M OFF
  -> コンデンサ放電待ち

重要なのは、K8が停止したと確認できるまで Tapo P110M を OFF にしないことです。

Home Assistant / Matter Server

Raspberry Pi 4B 上では Home Assistant OS ではなく、既存の Linux 環境に Docker Compose で Home Assistant と Matter Server を立てています。

services:
  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    container_name: homeassistant
    restart: unless-stopped
    network_mode: host
    privileged: true
    cap_add:
      - NET_ADMIN
      - NET_RAW
    environment:
      TZ: Asia/Tokyo
    volumes:
      - ./config:/config
      - /etc/localtime:/etc/localtime:ro
      - /run/dbus:/run/dbus:ro

  matterjs-server:
    image: ghcr.io/matter-js/matterjs-server:stable
    container_name: matterjs-server
    restart: unless-stopped
    network_mode: host
    environment:
      TZ: Asia/Tokyo
      STORAGE_PATH: /data
      PRIMARY_INTERFACE: eth0
      LISTEN_ADDRESS: 127.0.0.1
      NOBLE_BINDINGS: dbus
    volumes:
      - ./matter-data:/data
      - /run/dbus:/run/dbus:ro
    cap_add:
      - NET_RAW

Home Assistant では Matter 統合を追加し、Matter Server URL に次を指定します。

ws://127.0.0.1:5580/ws

Tapo P110M は Home Assistant 上では switch.smart_wi_fi_plug として扱っています。k8ctl からは Matter のノードIDではなく、Home Assistant の entity_id を操作します。

k8ctl の設定

制御スクリプトは Raspberry Pi 4B の ~/WS/k8ctl/k8ctl に置いています。

設定例:

K8_MAC=<mac-address>
K8_HOST=192.168.0.5
K8_IP=192.168.0.5
K8_SSH_USER=<user>

HA_URL=http://127.0.0.1:8123
HA_TOKEN=replace-with-home-assistant-long-lived-token
AG02_ENTITY_ID=switch.smart_wi_fi_plug

AG02_STABLE_WAIT=20
AG02_OFF_DISCHARGE_WAIT=30
AG02_COMMAND_WAIT=15
GPU_POST_DETECT_WAIT=0
K8_PING_WAIT=120
K8_SSH_WAIT=180
GPU_WAIT=60
SHUTDOWN_WAIT=180
POST_SHUTDOWN_WAIT=15
WATCH_INTERVAL=10
WOL_RETRIES=3
WOL_RETRY_INTERVAL=5

Home Assistant API で P110M を制御する

k8ctl は Home Assistant の REST API を叩いて Tapo P110M を ON/OFF します。

ha_api() {
  local method="$1"
  local path="$2"
  local data="${3:-}"
  curl -fsS -X "$method" \
    -H "Authorization: Bearer $HA_TOKEN" \
    -H "Content-Type: application/json" \
    --data "$data" \
    "$HA_URL$path"
}

ag02_on() {
  ha_api POST "/api/services/switch/turn_on" \
    "{\"entity_id\":\"$AG02_ENTITY_ID\"}" >/dev/null
}

ag02_off() {
  ha_api POST "/api/services/switch/turn_off" \
    "{\"entity_id\":\"$AG02_ENTITY_ID\"}" >/dev/null
}

変数名は AG02_ENTITY_ID にしていますが、実際に ON/OFF しているのは AG02 本体ではなく、AG02 へ給電している Tapo P110M です。

Wake-on-LAN でK8を起動する

K8起動は Magic Packet を投げます。

k8_wol_once() {
  python3 - "$K8_MAC" <<'PY'
import socket
import sys

mac = sys.argv[1].replace(":", "").replace("-", "").lower()
if len(mac) != 12:
    raise SystemExit(f"invalid MAC: {sys.argv[1]}")

payload = bytes.fromhex("ff" * 6 + mac * 16)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(payload, ("255.255.255.255", 9))
PY
}

SSH でK8を停止する

停止は Raspberry Pi 4B から K8 へ SSH し、shutdown -h now を実行します。

k8_poweroff() {
  ssh "$K8_SSH_USER@$K8_HOST" \
    "sudo -n /sbin/shutdown -h now" >/dev/null 2>&1 || true
}

K8側の sudoers では /sbin/shutdown のみを NOPASSWD にします。広く sudo 全体を許可する必要はありません。

外付けGPUの検出

GPU 検出は Intel ARC Pro B70 の型番や PCI ID に固定していません。

内蔵の起動用 GPU は boot_vga=1 として除外し、それ以外の VGA/3D controller が DRM render node を持ち、対応する /dev/dri/renderD* が存在するかを見ます。

gpu_present() {
  ssh "$K8_SSH_USER@$K8_HOST" '
    for dev in /sys/bus/pci/devices/*; do
      [ -r "$dev/class" ] || continue
      case "$(cat "$dev/class")" in
        0x030000|0x030200) ;;
        *) continue ;;
      esac

      [ "$(cat "$dev/boot_vga" 2>/dev/null || echo 0)" = "1" ] && continue

      for render in "$dev"/drm/renderD*; do
        [ -e "$render" ] || continue
        [ -e "/dev/dri/$(basename "$render")" ] && exit 0
      done
    done
    exit 1
  '
}

この判定なら、将来 GPU を差し替えても「外付けの利用可能な render node があるか」で見られます。

状態定義

k8ctl status は次のような出力を返します。

AG02=on
K8_PING=on
K8_SSH=yes
GPU=yes
STATE=S3

状態は以下のように定義しています。

状態 条件 意味 自動対応
S0 P110M=off / K8=off 完全停止 なし
S1 P110M=on / K8=off AG02側だけON P110Mは維持
S3 P110M=on / K8=on / GPU=yes 正常稼働 なし
S5 P110M=on / K8=on / GPU=no K8起動中だが外付けGPU未検出 K8を安全停止、P110Mは維持
S9 P110M=off / K8=on 危険状態 K8を安全停止

最も避けたいのは S9 です。これは K8 が ON のまま、AG02 / dGPU 側への給電が OFF の状態です。

一方、S1 は手動再起動や途中状態の可能性があるため、監視処理では P110M を OFF にしません。安全側に倒すなら、外付けGPU側の電源は勝手に切らない方がよいです。

監視サービス

k8ctl watch は定期的に recover を実行します。

watch_system() {
  while true; do
    (
      if flock -n 9; then
        recover_system || true
      fi
    ) 9>"$LOCK_FILE"
    sleep "$WATCH_INTERVAL"
  done
}

startstoprestartrecover は同じ lock file を使います。手動操作中に監視サービスが同時に復旧処理を走らせる事故を避けるためです。

systemd service はシステムサービスとして動かしています。

[Unit]
Description=k8ctl safety monitor
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
User=<user>
WorkingDirectory=/home/<user>/WS/k8ctl
ExecStart=/home/<user>/WS/k8ctl/k8ctl watch
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Windows 11 からの操作

Windows 11 側は .cmd で Raspberry Pi 4B に SSH するだけです。

@echo off
setlocal
chcp 65001 >nul
title K8 START

echo === K8 START ===
echo.

ssh pi4 "cd ~/WS/k8ctl && ./k8ctl start"
set "EXITCODE=%ERRORLEVEL%"

echo.
if "%EXITCODE%"=="0" (
  echo [OK] K8 start completed.
) else (
  echo [ERROR] K8 start failed. ExitCode=%EXITCODE%
)
echo.
pause
exit /b %EXITCODE%

停止と再起動は最後のコマンドだけ変えます。

ssh pi4 "cd ~/WS/k8ctl && ./k8ctl stop"
ssh pi4 "cd ~/WS/k8ctl && ./k8ctl restart"

Windows 側は薄いラッパーにして、安全制御は Raspberry Pi 4B 側に集約しています。これにより、どの端末から実行しても同じ電源シーケンスになります。

Raspberry Pi 4B でなくてもよい

今回は Raspberry Pi 4B を使っていますが、役割として必要なのは次の条件を満たす常時稼働の Linux ホストです。

  • Docker または同等の方法で Home Assistant / Matter Server を動かせる
  • Home Assistant API にローカルからアクセスできる
  • Wake-on-LAN の UDP ブロードキャストを送れる
  • K8 へ SSH できる
  • systemd などで監視プロセスを常駐できる

そのため、最近の Linux ベース NAS でも同様の構成は組める可能性があります。たとえば Docker が使える NAS なら、Home Assistant と Matter Server を NAS 上に置き、k8ctl 相当のスクリプトを cron や systemd timer/service で動かせます。

ただし NAS で代替する場合は、Matter/Bluetooth/IPv6 multicast 周り、host network の扱い、WOL ブロードキャストの到達性、停電復帰時の起動順序を確認する必要があります。ここを見ずに移植すると、肝心の「電源制御の信頼性」が落ちます。

まとめ

OCuLink 接続の外付けGPU環境では、性能以前に電源順序を信用できるようにするのが大事です。

今回の設計では、Raspberry Pi 4B を制御点として次を守っています。

  • 起動時は Tapo P110M を先に ON し、AG02 / dGPU 側を安定させてから K8 を起動
  • 停止時は K8 の shutdown 完了を確認してから Tapo P110M を OFF
  • P110M OFF / K8 ON は危険状態として K8 を安全停止
  • P110M ON / K8 ON / GPU なしも異常として K8 を安全停止
  • 監視処理では P110M を勝手に OFF にしない
  • Windows 側は SSH ラッパーに留め、安全制御は常時稼働 Linux ホスト側へ集約

外付けGPUの電源制御は地味ですが、失敗した時の被害が大きい部分です。OCuLink eGPU 環境を常用するなら、ここを自動化しておく価値はかなりあります。

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?