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-release
PRETTY_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 info
Server 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'

結果として、bluezendpoint に関するログが出ていませんでした。

一方で、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側で接続します。

bluetoothctl
power 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:D9
Connected: 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 Linger
Linger=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 argumentsrequired の行を見て、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のスピーカーへ音を出せるようになりました。