Raspberry Pi(jessie)に繋いだスピーカーがOS再起動時にボンというのを抑止したい(Perlスクリプト)


2016/03/12追記:
Pythonでもスクリプトを作りました。ロジックの説明はこちらのほうが優しいです。ご一読下さい。

経緯

我が家のRaspi君にUSB経由で音を出せるDACを接続してAirPlayサーバーとして動かしていますが、諸般の事情により毎日午前4時にOSの自動再起動してます。
その際、USB DACに過電流(?)が流れるのか、停止時に3回ほど「ボン」と嫌な感じの音がしてスピーカーとアンプがなんとなく可哀想なのでどうにかすることにしました。
なお、「アンプの電源切っとけばいいじゃん」というツッコミは無しでお願いします。そんなの毎日やるの面倒くさい。

結論 : 該当のUSBをunbindしてから再起動すればOK!

USBのUnbindとはあまり聞き慣れなかったのですが、この処置により該当のデバイスをソフトウェア的に接続を解除してくれているようです。
元はここの記事で知ったんですが、意外とunbind自体の記事は色々あるようですね。
とにかく、Unbindしてから再起動することで、例の「ボン」を出さずにRaspi君の再起動が可能になりました。

以下よりunbindについて具体的に見てみましょう。

前提

うちのRaspi君の環境です。(今回影響しそうな箇所のみ)

基本的なunbindの書き方

$ echo -n '1-1.1' > /sys/bus/usb/drivers/usb/unbind

この1ー1.1というのが、対象とするUSBの場所です。
最初の数字がバス番号で、それ以降は途中のHubの番号、最後の数字が最終的なUSBのポート番号です。なので、USB Hubを途中にいくつも入れる場合は、例えばですが、1-1.1.1.1みたいに間が増えます。こちらが詳しいです。

自分の環境がどうなっているか確認するには、lsusb -tを使って見れます。

$ lsusb -t
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=dwc_otg/1p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/5p, 480M
        |__ Port 1: Dev 3, If 0, Class=Vendor Specific Class, Driver=smsc95xx, 480M
        |__ Port 2: Dev 4, If 0, Class=Vendor Specific Class, Driver=ftdi_sio, 12M
        |__ Port 3: Dev 5, If 0, Class=Audio, Driver=snd-usb-audio, 12M
        |__ Port 3: Dev 5, If 1, Class=Audio, Driver=snd-usb-audio, 12M
        |__ Port 3: Dev 5, If 2, Class=Audio, Driver=snd-usb-audio, 12M
        |__ Port 3: Dev 5, If 3, Class=Human Interface Device, Driver=usbhid, 12M

なので最初のUnbindのコマンド例では、1ー1.1としていますが、実際には別の値を入れる必要があります。
lsusb -tすると分かる通り、RaspberryPiでは最初の(内部的な)Hubの直下にはsmsc95xxが来ます。なので、1-1.1になることはまず無いでしょう。

上記のlsusb -tの結果例の、Driver=snd-usb-audioという文字列を含む行が今回の対象となるDACのPort番号なので、私の環境では

$ echo -n '1-1.3' > /sys/bus/usb/drivers/usb/unbind

となります。

実際の運用

まぁ上で書いた事くらいはちょっと調べればすぐ分かるのでいいんですが、問題はその自動化です。
別に決め打ちで、unbindの行だけ実行してしまってもいいんですが、私の場合は構成を色々と変える為、Hub番号やPort番号が変わらない保証がないので、その度にコマンド再登録とかやりたくない。本当はPerlの勉強がてら何か書きたかっただけ。

なので、スクリプトを書きました。

親スクリプト

dac_unbind_then_reboot.sh
#!/bin/bash

UNBIND="/sys/bus/usb/drivers/usb/unbind"
PORT="/home/pi/bin/get_dac_port_num2.sh"
echo "unbind DAC `$PORT`"
$PORT > $UNBIND
echo "reboot starting at `date`"
/sbin/reboot 

親スクリプト解説

子スクリプトから取得したPORT変数(上の例で言えば中身には1-1.3が入っている)をUNBINDコマンドに突っ込んでいます。
元々が「Raspi君の再起動の時にボンってなるのを抑止したい」って要件ですので、最後にリブートしてます。
特に難しい記述は無いと思ってますので後は割愛です。
子スクリプトは以下で解説します。

子スクリプト

get_dac_port_num2.sh
#!/bin/bash

lsusb -t | perl -nle '                                        # (1)
    @USB[0]=0+$1."-" if( /Bus (\d*)\.Port/);                  # (2)
    @USB[length($1)]=0+$2."." if( /^(\s*)\|__.*Port (\d*): .*Class=Hub/);  # (3)
    if ( /^(\s*)\|__.*Port (\d*): .*Driver=snd-usb-audio/ ){  # (4)
        print join("",@USB[0..(length($1)-1)]) . (0+$2);      # (5)
        last;                                                 # (6)
    }
'

子スクリプト解説

ざっくり解説としては、Bus番号とHub番号とPort番号を取得して、それを

x-x.x

という形式で出力する処理です。(xは数字)

ここで、今の環境では途中にUSB Hubを噛ませていないのでシンプルですが、Hubが増えると間にx-x.x.xと番号が増えていきますので、それに対応しないといけません。
かつ、複数のHubが並列にあった場合にも正しく出力する為にちょっとした処理入ってます。

行毎の説明(番号はスクリプトのコメント内番号に対応)
1. lsusb -tの出力の各行について以下の処理実行します。
2. 「Bus 数字(複数桁).Port」という文字列を発見したら、その数字に0を足して「-」(ハイフン)という文字列を足して配列「USB」の0番目に代入する。(BUSは必ず最上位という前提の為、0番目。)0を足す事でそこまでは数字として扱いつつ、その後に文字列「-」を足すことで最終的に文字列型として変数に入れてます。
3. ある行に、「Port 数字(複数桁): * Class=Hub」というような文字列を発見したら、配列「USB」にPortの後に続く数字、および「.」を追加するが、その際の配列のインデックスは、その行のPortより前に有る空白文字の数となる。(空白が4つなら、@USB[4]ということ。)
4. 「Driver=snd-usb-audio」という文字列を発見したら
5. 配列「USB」の0から、上記4.の文字列を含む行のPortより前に有る空白文字の数マイナス1の数の位置までの中身を「」(ヌル文字)というセパレータでくっつけて出力し、さらに4.の行のPortの後の数字を出力する。
6. 処理を終了する。(最初の1回の出力だけで十分なので。)

「見づれえよ!」というツッコミもあるかもですが、Perlでどんな事が出来るのか知りたい厨二病の為その辺はご愛嬌。。Perlのスクリプト(というより殆ど1行コマンド)をちゃんと書いたのは初めてなので、ShellやPHPよりも大胆に色々省略してかけるということが驚きでした。

Perl書いてて思ったこと

細かい説明は割愛しますが、上記スクリプトでは変数代入時にIfを省略(手抜き?)した使い方や、Ifのカッコの中の検索で手抜きを行ったり、文字としての数字を+0で数字型に変換したりしてます。
Perlのワンライナーな人のスクリプトが読みづらい理由は、このような省略が出来すぎる事に有るんでしょうが、使ってる側としては何とも便利ですね。。
個人用としては有りですが、余りPerl使いがいない職場ではここまでやったらメンテが難しくなるので良くないです。良くない。でもなんかすごい。痛気持ちいい。

実装です

さて、上記のスクリプトを毎日動かします。
raspbianではデフォルトでCronが動かないのでそれをONにする必要があります。

$ sudo /etc/init.d/cron start
[ ok ] Starting periodic command scheduler: cron.

また、スクリプトを作ったら忘れずにchmod ugo+x [そのファイル名]して実行可能にします。(よく忘て、テスト実行時に「ありゃ」ってなる。)

その上で、sudo crontab -eなどのお好きな方法でCron編集します。私の場合は以下のようなエントリーを追加しました。

00 4 * * * [シェルのフルパス] > /tmp/reboot.log

ここの/tmp/reboot.logは、まぁ別に/dev/nullにしても良いんですが、一応ちゃんと動いているか見たかったので入れました。

これで、毎日静かぁにリブートしてくれるはずです。めでたしめでたし。

最後に

某所で、Perlとかsedとかawkとか使うって話をしたら、

そんなのおじさんですよ。(笑)

と言われ若干ショックです。
でも良いんです。私が使ってる業務用UnixにはPythonとかオシャレなの入ってないもん。Perlとかsedとかawkとかなら大体のUnix系OSに入ってるんだもん。と、自分を慰めました。

Perlスクリプト作成にあたり、職場のOさんに色々と助言頂き、感謝x10です。

何かツッコミあったらコメントに宜しくお願いします。