背景
ミニ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
}
start、stop、restart、recover は同じ 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 環境を常用するなら、ここを自動化しておく価値はかなりあります。