Raspberry Pi Zero 2 W から Echo Show 5 第1世代をBluetoothスピーカーとして使うまで
これはAIとともに記載したブログです。自分の備忘録として記載しています
はじめに
Raspberry Pi Zero 2 W で音声アシスタントのようなものを作るにあたり、音声出力先として手元にあった Echo Show 5 第1世代 を使えないか試しました。
Echo Show 5 第1世代には3.5mmオーディオジャックがありますが、これは 音声入力ではなく音声出力 です。
つまり、Raspberry Pi からケーブルでEcho Show 5へ音を入れることはできません。
そのため今回は、以下の構成を目指しました。
Raspberry Pi Zero 2 W ↓ Bluetooth A2DP Echo Show 5 第1世代のスピーカー
最終的には、Pythonの常駐daemonから音声を鳴らせるところまで設定します。
環境
今回の環境は以下です。
cat /etc/os-releasePRETTY_NAME="Debian GNU/Linux 13 (trixie)" VERSION_ID="13" VERSION_CODENAME=trixie
主な構成は以下です。
Raspberry Pi Zero 2 W Debian GNU/Linux 13 trixie PipeWire 1.4.2 WirePlumber 0.5.8 BlueZ 5.82 Echo Show 5 第1世代
音声サーバーは以下のように、PulseAudio互換のPipeWireでした。
pactl infoServer Name: PulseAudio (on PipeWire 1.4.2) Default Sink: alsa_output.platform-soc_sound.stereo-fallback
最初に出たエラー
Echo Show 5をBluetoothペアリング待機にし、Raspberry Pi側から接続しようとすると、以下のエラーが出ました。
Failed to connect: org.bluez.Error.Failed br-connection-profile-unavailable
このエラーは、ざっくり言うと、
Bluetooth機器としては見えているが、
使える音声プロファイルが見つからない
という状態です。
つまり、単にペアリングできていないというより、A2DPなどのBluetooth音声プロファイルが正しく登録されていない可能性があります。
Echo Show 5側はAudio Sinkとして見えていた
まず、Echo Show 5がRaspberry Piから見て「音声の送り先」になっているか確認しました。
bluetoothctl info F4:03:2A:7D:0A:D9
結果は以下です。
Device F4:03:2A:7D:0A:D9 Name: Echo Show 5-14E Paired: yes Bonded: yes Trusted: yes Connected: no UUID: Audio Source UUID: Audio Sink UUID: A/V Remote Control Target UUID: A/V Remote Control
ここで重要なのはこれです。
UUID: Audio Sink
Echo Show 5側は、Raspberry Piから見て Bluetoothスピーカーとして見えている ことがわかりました。
つまり問題はEcho Show 5側ではなく、Raspberry Pi側にありそうです。
Raspberry Pi側にAudio Sourceが出ていなかった
次に、Raspberry Pi自身がBluetooth音声を送信できる状態か確認しました。
bluetoothctl show
この時点では以下のような結果でした。
Controller 88:A2:9E:B3:68:12 Name: raspizero2 Powered: yes UUID: SIM Access UUID: PnP Information UUID: A/V Remote Control Target UUID: A/V Remote Control
ここに本来ほしいものはこれです。
UUID: Audio Source
しかし、これがありませんでした。
つまり状態としてはこうです。
Echo Show 5: Audio Sink あり → OK Raspberry Pi: Audio Source なし → NG
このため、bluetoothctl connect しても、
br-connection-profile-unavailable
になっていたわけです。
PipeWire / WirePlumber / BlueZ関連パッケージを確認
必要なパッケージを確認しました。
dpkg -l | grep -E 'libspa-0.2-bluetooth|wireplumber|pipewire|bluez'
結果は以下のような状態でした。
ii bluez ii libspa-0.2-bluetooth ii pipewire ii pipewire-audio ii pipewire-pulse ii wireplumber
特に重要なのはこれです。
libspa-0.2-bluetooth
これはPipeWireでBluetooth音声を扱うためのプラグインです。
さらに、実体ファイルも確認しました。
find /usr/lib -name '*bluez*' -o -name '*bluetooth*' | grep -E 'spa|pipewire|wireplumber'
結果として以下が存在していました。
/usr/lib/aarch64-linux-gnu/spa-0.2/bluez5/libspa-bluez5.so
つまり、Bluetooth音声プラグイン自体はインストール済みでした。
WirePlumberがBlueZ monitorを読んでいなかった
次にWirePlumberのログを確認しました。
journalctl --user-unit=wireplumber.service -b --no-pager -l | grep -i -E 'bluez|bluetooth|spa|monitor|endpoint|media'
結果として、bluez や endpoint に関するログが出ていませんでした。
一方で、WirePlumberの設定ファイル内には monitor.bluez 自体は存在していました。
grep -RniE 'monitor.bluez|bluez5|api.bluez5|bluetooth' \ /usr/share/wireplumber \ /etc/wireplumber \ ~/.config/wireplumber 2>/dev/null
出力には以下のような行がありました。
/usr/share/wireplumber/scripts/session-services.lua:10: ["monitor.bluez"] = { "bluetooth.audio", "api.bluez" } /usr/share/wireplumber/scripts/monitors/bluez.lua /usr/share/wireplumber/wireplumber.conf:372: provides = monitor.bluez
つまり、BlueZ monitorの機能は存在しているが、現在のプロファイルでは有効になっていないようでした。貼り付けられたテキスト(1 点).txtTXT
WirePlumber設定を修正
今回の音声サーバーは、pi ユーザーのPipeWire/WirePlumberとして動いていました。
User Name: pi Server Name: PulseAudio (on PipeWire 1.4.2)
そのため、まずはユーザー設定として以下に設定を置きました。
/home/pi/.config/wireplumber/
/usr/share/wireplumber/ はパッケージ管理下のデフォルト設定なので直接編集しません。
検証段階では、ユーザーごとの設定である ~/.config/wireplumber/ に置くのが安全です。
最終的に運用を固めるなら、/etc/wireplumber/ に移してもよいです。
Bluetoothロール設定
以下のファイルを作成しました。
mkdir -p /home/pi/.config/wireplumber/wireplumber.conf.d nano /home/pi/.config/wireplumber/wireplumber.conf.d/50-bluez.conf
内容は以下です。
monitor.bluez.properties = { bluez5.roles = [ a2dp_source a2dp_sink hfp_hf hfp_ag ] bluez5.codecs = [ sbc sbc_xq ] bluez5.enable-sbc-xq = true bluez5.enable-msbc = true bluez5.enable-hw-volume = true }
ここで重要なのは、
a2dp_source
です。
Raspberry PiからEcho Show 5へ音を送るには、Raspberry Pi側が A2DP Source になる必要があります。
PipeWire / WirePlumber / Bluetoothを再起動
設定後、以下の順で再起動しました。
systemctl --user stop wireplumber pipewire-pulse pipewire sudo systemctl restart bluetooth systemctl --user start pipewire systemctl --user start pipewire-pulse systemctl --user start wireplumber sleep 5
そして再度確認します。
bluetoothctl show
ここに以下が出ることを確認します。
UUID: Audio Source
これが出れば、Raspberry Pi側がBluetooth音声送信側として登録できています。
Echo Show 5へ接続
Echo Show 5側でBluetoothペアリング待機にします。
アレクサ、Bluetoothをペアリングして
Raspberry Pi側で接続します。
bluetoothctlpower on agent on default-agent scan on pair F4:03:2A:7D:0A:D9 trust F4:03:2A:7D:0A:D9 connect F4:03:2A:7D:0A:D9
接続後、状態を確認します。
bluetoothctl info F4:03:2A:7D:0A:D9Connected: yes
音声出力先を確認
Bluetooth接続ができても、それだけではPythonやpaplayの音がEcho Show 5から出るとは限りません。
PipeWire/PulseAudio側にBluetooth音声出力先が作られているか確認します。
pactl list short sinks
以下のような bluez_output が出ればOKです。
bluez_output.F4_03_2A_7D_0A_D9.a2dp-sink
デフォルト出力にします。
pactl set-default-sink bluez_output.F4_03_2A_7D_0A_D9.a2dp-sink
音量とミュートも調整します。
pactl set-sink-mute @DEFAULT_SINK@ 0 pactl set-sink-volume @DEFAULT_SINK@ 85%
音を鳴らすテスト
まずは speaker-test で確認しました。
speaker-test -t wav -c 2
Echo Show 5から「Front Left」「Front Right」のような音声が出れば成功です。
WAVファイルを鳴らす場合は、
paplay test.wav
MP3なら、
sudo apt install -y mpg123 mpg123 sample.mp3
を使います。
alsamixerではなくpactl / wpctlを使う
Bluetoothスピーカーの音量は、基本的に alsamixer では調整しません。
alsamixer は主にALSAの物理サウンドカード向けです。
今回のBluetooth出力はPipeWire/PulseAudio側のsinkなので、以下のように調整します。
pactl set-sink-volume @DEFAULT_SINK@ 85% pactl set-sink-mute @DEFAULT_SINK@ 0
PipeWireの wpctl を使うなら以下です。
wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.85 wpctl set-mute @DEFAULT_AUDIO_SINK@ 0
Echo Show 5本体側の音量も影響するので、必要に応じてAlexaに話しかけます。
アレクサ、音量を8にして
PythonからEcho Show 5で音を鳴らす
Pythonから直接Bluetoothを操作するのではなく、PipeWire/PulseAudioのデフォルト出力へ音を流すのが簡単です。
WAVなら paplay をPythonから呼ぶのが安定しました。
import subprocess from pathlib import Path def play_wav(path: str): wav_path = Path(path) if not wav_path.exists(): raise FileNotFoundError(wav_path) subprocess.run(["paplay", str(wav_path)], check=True) play_wav("/home/pi/app/sounds/test.wav")MP3なら
mpg123を呼びます。import subprocess from pathlib import Path def play_mp3(path: str): mp3_path = Path(path) if not mp3_path.exists(): raise FileNotFoundError(mp3_path) subprocess.run(["mpg123", "-q", str(mp3_path)], check=True) play_mp3("/home/pi/app/sounds/sample.mp3")
Python daemonをsystemd user serviceで動かす
最終的には、Pythonプログラムを常駐daemonとして動かしたいので、systemd user serviceにしました。
ポイントは、rootのsystem serviceではなく、PipeWire/WirePlumberが動いているユーザーで実行することです。
今回なら pi ユーザーです。
pi の systemd --user ├─ pipewire ├─ pipewire-pulse ├─ wireplumber └─ speaker-daemon.serviceログインしていなくてもuser serviceを起動するために、lingerを有効化します。
sudo loginctl enable-linger pi
確認します。
loginctl show-user pi | grep LingerLinger=yes
serviceファイル
以下に作成します。
mkdir -p /home/pi/.config/systemd/user nano /home/pi/.config/systemd/user/speaker-daemon.service
内容例です。
[Unit] Description=Speaker daemon using PipeWire Bluetooth audio After=echo-bluetooth-watchdog.service pipewire.service pipewire-pulse.service wireplumber.service Wants=echo-bluetooth-watchdog.service pipewire.service pipewire-pulse.service wireplumber.service [Service] Type=simple WorkingDirectory=/home/pi/wake_words ExecStart=/home/pi/wake_words/.venv/bin/python /home/pi/wake_words/src/smart_speaker.py --model /home/pi/wake_words/Hey_Ninja_20260527_021038.onnx --device 1 --threshold 0.65 --wake-arm-blocks 5 --voice marin Restart=always RestartSec=5 Environment=XDG_RUNTIME_DIR=/run/user/1000 Environment=PULSE_SERVER=unix:/run/user/1000/pulse/native Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=default.target
[Service] の意味
Type=simple
ExecStart のプロセスをそのままメインプロセスとして扱います。Pythonの常駐スクリプトなら基本これでOKです。
WorkingDirectory=/home/pi/wake_words
Python実行時のカレントディレクトリです。相対パスはこのディレクトリ基準になります。
ExecStart=...
実際に起動するコマンドです。
venvを使う場合、activate は不要で、venv内のPythonを直接指定します。
ExecStart=/home/pi/wake_words/.venv/bin/python ...Restart=always RestartSec=5
Pythonプログラムが落ちたら5秒後に再起動します。
Environment=XDG_RUNTIME_DIR=/run/user/1000 Environment=PULSE_SERVER=unix:/run/user/1000/pulse/native
daemonから pi のPipeWire/PulseAudioソケットへ接続するために明示しています。
Environment=PYTHONUNBUFFERED=1
Pythonのログをjournalで見やすくするためです。
systemd user serviceの操作
反映します。
systemctl --user daemon-reload systemctl --user enable --now speaker-daemon.service
状態確認です。
systemctl --user status speaker-daemon.service --no-pager -l
ログ確認は以下です。
journalctl --user-unit=speaker-daemon.service -f
環境によっては、
journalctl --user -u speaker-daemon.service -f
では No journal files were found. になることがありました。
その場合は --user-unit= を使うと見られました。
status=2で落ちる場合
一度、以下のような状態になりました。
Active: activating (auto-restart) (Result: exit-code) ExecStart=... code=exited, status=2
Pythonで status=2 の場合、よくあるのは argparse の引数エラーです。
まず、systemdに書いたのと同じコマンドを手動で実行します。
cd /home/pi/wake_words /home/pi/wake_words/.venv/bin/python \ /home/pi/wake_words/src/smart_speaker.py \ --model /home/pi/wake_words/Hey_Ninja_20260527_021038.onnx \ --device 1 \ --threshold 0.65 \ --wake-arm-blocks 5 \ --voice marin
また、引数一覧を確認します。
/home/pi/wake_words/.venv/bin/python \ /home/pi/wake_words/src/smart_speaker.py \ --help
error: unrecognized arguments や required の行を見て、ExecStart の引数を修正します。
Bluetooth切断に備えてwatchdogを作る
Echo Show 5とのBluetooth接続は、時間経過や再起動などで切れることがあります。
そこで、接続が切れていたら自動再接続するwatchdogを作りました。
mkdir -p /home/pi/bin
nano /home/pi/bin/echo-bluetooth-watchdog.sh
内容です。
#!/bin/bash MAC="F4:03:2A:7D:0A:D9" VOLUME="85%" log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" } is_connected() { bluetoothctl info "$MAC" | grep -q "Connected: yes" } has_bluetooth_sink() { pactl list short sinks | grep -q "bluez_output" } connect_echo() { log "Echo Show 5 is disconnected. Trying to reconnect..." bluetoothctl <<EOF power on agent on default-agent trust $MAC connect $MAC quit EOF sleep 3 } setup_sink() { SINK=$(pactl list short sinks | awk '/bluez_output/ {print $2; exit}') if [ -n "$SINK" ]; then log "Setting default sink: $SINK" pactl set-default-sink "$SINK" pactl set-sink-mute "$SINK" 0 pactl set-sink-volume "$SINK" "$VOLUME" else log "Bluetooth sink not found yet." fi } while true; do if is_connected; then if ! has_bluetooth_sink; then log "Bluetooth connected, but audio sink not found." setup_sink fi else connect_echo if is_connected; then log "Reconnect succeeded." setup_sink else log "Reconnect failed." fi fi sleep 15 done
実行権限を付けます。
chmod +x /home/pi/bin/echo-bluetooth-watchdog.sh
watchdogのservice化
nano /home/pi/.config/systemd/user/echo-bluetooth-watchdog.service
内容です。
[Unit] Description=Echo Show 5 Bluetooth reconnect watchdog After=pipewire.service pipewire-pulse.service wireplumber.service Wants=pipewire.service pipewire-pulse.service wireplumber.service [Service] Type=simple ExecStart=/home/pi/bin/echo-bluetooth-watchdog.sh Restart=always RestartSec=5 Environment=XDG_RUNTIME_DIR=/run/user/1000 Environment=PULSE_SERVER=unix:/run/user/1000/pulse/native [Install] WantedBy=default.target
有効化します。
systemctl --user daemon-reload systemctl --user enable --now echo-bluetooth-watchdog.service
ログを確認します。
journalctl --user-unit=echo-bluetooth-watchdog.service -f
setup_sinkでpactlを呼ぶ理由
bluetoothctl info で、
Connected: yes
になっていても、それだけでは音が出るとは限りません。
Bluetooth接続と、PipeWire/PulseAudio側の音声出力先は別です。
Bluetooth接続済み
≠
Pythonやpaplayの出力先がEcho Show 5になっている
そのため、再接続後に以下を行います。
pactl set-default-sink "$SINK" pactl set-sink-mute "$SINK" 0 pactl set-sink-volume "$SINK" "$VOLUME"
ただし、毎回実行する必要はありません。
基本は 再接続成功時、または 接続済みなのにbluez_outputが見えない時 だけで十分です。
まとめ
今回のポイントは以下でした。
Echo Show 5の3.5mmジャックは入力ではない
Raspberry PiからEcho Show 5へ音を出すにはBluetoothを使う
br-connection-profile-unavailable はA2DPプロファイル未登録が原因になりやすい
Echo側は Audio Sink、Pi側は Audio Source が必要
PipeWire / WirePlumber / libspa-0.2-bluetooth が必要
daemonは root ではなく、音声セッションを持つユーザーで動かす
Pythonからは paplay や mpg123 を呼ぶのが簡単
Bluetooth切断に備えてwatchdogを用意すると安定する
最終構成は以下です。
Raspberry Pi Zero 2 W ├─ Debian trixie ├─ PipeWire / WirePlumber ├─ echo-bluetooth-watchdog.service │ └─ Echo Show 5へ自動再接続 └─ speaker-daemon.service └─ Python音声アシスタント本体 ↓ paplay / pactl Echo Show 5
これで、Raspberry Pi Zero 2 W上のPythonプログラムから、Echo Show 5のスピーカーへ音を出せるようになりました。