2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

YAMAHAのAVアンプ RX-S601 を操作してみる

Posted at

概要

YAMAHAのAVアンプ RX-S601 をAndroidの自作アプリから操作してみたので、その辺りの経緯について書いています。

背景

YAMAHAのAVアンプは、携帯端末から操作できるモバイルアプリが提供されています。

AV CONTROLLER

以前から個人でYAMAHAのAVアンプRX-S601を使用していたので、このアプリで操作していました。ただ、携帯端末の音楽をアンプのスピーカーから流そうと思った時に、入力ソースをBluetoothに切り替える必要があり、その都度アプリを開いて操作するのが面倒でした。
そこで、自分で入力ソースを切り替える仕組みが作れないかと思い、Androidアプリ上で実装してみた次第です。

通信内容の調査

とりあえず、APIの仕様がどこかに無いかと思ったのですが、特に公開はされていないようでした。GitHubにYAMAHAのAVアンプを操作するソースがいくつかあったのですが、自分の機種で使えるのか分からず、実際に通信の中身を見た方が早そうだったので、ツールで通信をキャプチャすることにしました。

今回は、tPacketCaptureというアプリでキャプチャしました。保存されたデータはWiresharkで直接見る事ができます。以下は実際のサンプル画像です。(AVアンプのIPアドレスは192.168.0.68に設定されています)
image.png

Bluetoothの切替え操作

通信内容の確認

YAMAHAのアプリでBluetooth切替えした時のパケットをキャプチャしてみたところ、次のような通信をしていました(必要そうな箇所を抜粋)。

URL: http://[AVアンプのアドレス]/YamahaRemoteControl/ctrl
メソッド: POST

リクエストヘッダ(一部抜粋):

  • CONTENT-TYPE: text/xml; charset="utf-8"
  • User-Agent: AV Controller/5.50 (Android)

リクエストデータ(整形済み):

<?xml version="1.0" encoding="utf-8" ?>
<YAMAHA_AV cmd="PUT">
    <Main_Zone>
        <Input>
            <Input_Sel>Bluetooth</Input_Sel>
        </Input>
    </Main_Zone>
</YAMAHA_AV>

レスポンスデータ (整形済み)

<YAMAHA_AV rsp="PUT" RC="0">
    <Main_Zone>
        <Input>
            <Input_Sel></Input_Sel>
        </Input>
    </Main_Zone>
</YAMAHA_AV>

HTTPSではなくHTTPなので、生データが見えました。パッと見たところ、リクエストデータのInput_Selの値が、切替える入力ソースを示しているようです。レスポンスデータは特に必要なデータは無さそうです。

上記の通信をCurlで実行してみます。

> curl -X POST -d '<YAMAHA_AV cmd="PUT"><Main_Zone><Input><Input_Sel>Bluetooth</Input_Sel></Input></Main_Zone></YAMAHA_AV>' 192.168.0.68/YamahaRemoteControl/ctrl

AVアンプの入力ソースがBluetoothに切り替わることが確認出来ました。ヘッダ情報は無くても動くようです。

現在の入力ソースを見る

次に現在の入力ソースを取得してみます。アプリを開いてデバイスを選択したとき、入力ソースが画面に表示されるので、このタイミングで状態を取得していると思われます。この時の通信内容を見ると、それらしい物がありました。

リクエストデータ(整形済み):

<?xml version="1.0" encoding="utf-8" ?>
<YAMAHA_AV cmd="GET">
    <Main_Zone>
        <Basic_Status>GetParam</Basic_Status>
    </Main_Zone>
</YAMAHA_AV>

レスポンスデータ (整形済み)

<YAMAHA_AV rsp="GET" RC="0">
    <Main_Zone>
        <Basic_Status>
            <Power_Control>
                <Power>On</Power>
                <Sleep>Off</Sleep>
            </Power_Control>
            <Volume>
                <Lvl>
                    <Val>-270</Val>
                    <Exp>1</Exp>
                    <Unit>dB</Unit>
                </Lvl>
                <Mute>Off</Mute>
                <Subwoofer_Trim>
                    <Val>30</Val>
                    <Exp>1</Exp>
                    <Unit>dB</Unit>
                </Subwoofer_Trim>
                <Scale>dB</Scale>
            </Volume>
            <Input>
                <Input_Sel>HDMI1</Input_Sel>
                <Input_Sel_Item_Info>
                    <Param>HDMI1</Param>
                    <RW>RW</RW>
                    <Title>HDMI1</Title>
                    <Icon>
                        <On>/YamahaRemoteControl/Icons/icon004.png</On>
                        <Off></Off>
                    </Icon>
                    <Src_Name></Src_Name>
                    <Src_Number>1</Src_Number>
                </Input_Sel_Item_Info>
            </Input>
            <Surround>
                <Program_Sel>
                    <Current>
                        <Straight>On</Straight>
                        <Enhancer>On</Enhancer>
                        <Sound_Program>Hall in Munich</Sound_Program>
                    </Current>
                </Program_Sel>
                <_3D_Cinema_DSP>Off</_3D_Cinema_DSP>
            </Surround>
            <Party_Info>Off</Party_Info>
            <Sound_Video>
                <Tone>
                    <Bass>
                        <Val>0</Val>
                        <Exp>1</Exp>
                        <Unit>dB</Unit>
                    </Bass>
                    <Treble>
                        <Val>0</Val>
                        <Exp>1</Exp>
                        <Unit>dB</Unit>
                    </Treble>
                </Tone>
                <Direct>
                    <Mode>Off</Mode>
                </Direct>
                <HDMI>
                    <Standby_Through_Info>On</Standby_Through_Info>
                    <Output>
                        <OUT_1>On</OUT_1>
                    </Output>
                </HDMI>
                <Extra_Bass>Off</Extra_Bass>
                <Adaptive_DRC>Auto</Adaptive_DRC>
            </Sound_Video>
        </Basic_Status>
    </Main_Zone>
</YAMAHA_AV>

ここで各種パラメータを一斉に取得しているようです。XMLの /YAMAHA_AV/Main_Zone/Basic_Status/Input/Input_Sel にある値が、現在の入力ソースを指し示しているようです。実際、入力ソースを切り替えてみると、ここの値が変化します。

上記をCurlで試すと以下のコマンドになります。実行すると、レスポンスが返ってくることが確認できます。

curl -X POST -d '<?xml version="1.0" encoding="utf-8" ?><YAMAHA_AV cmd="GET"> <Main_Zone><Basic_Status>GetParam</Basic_Status></Main_Zone></YAMAHA_AV>' 192.168.0.68/YamahaRemoteControl/ctrl

通信の実装

Androidアプリで、上記通信を行ってみます。例によって、OkHTTPとRetrofitの組合せで行いました。APIがJSONではなくXMLですので、XMLのコンバータがあった方が良さそうです。その辺りについては以前、AndroidからXMLデータ形式のWeb APIを使うに詳細を書いているので省略します。
とりあえず、リクエストのXMLをTikXMLのデータモデルを書くとこんな感じでしょうか。(面倒くさいので、汎用性を求めないならXMLを直書きした方が早い気がしますが。)

またHTTP通信なので、targetSdkVersionが28以上の場合、android:usesCleartextTraffic="true"を設定するか、network_security_config.xmlを設定するなどして、HTTPの通信を許可する必要があります。


@Xml(name = "YAMAHA_AV")
data class CtrlReq(
    @Attribute(name = "cmd")
    val cmd: String,
    @Element(name = "Main_Zone")
    val mainZone: MainZoneReq
)

@Xml
data class MainZoneReq(
    @Element(name = "Input")
    val input: InputReq? = null,
    @PropertyElement(name = "Basic_Status")
    val basicStatus: String? = null
)

@Xml
data class InputReq(
    /**
     * Select input ( HDMIx, BLUETOOTH, ... )
     */
    @PropertyElement(name = "Input_Sel")
    val inputSel: String? = null
)

レスポンスのデータモデルは長くなるので省略します。
インターフェースはこんな感じ。

interface YamahaRemoteControlApi {

    @POST("ctrl")
    suspend fun postCtrl(@Body ctrlReq: CtrlReq): CtrlRes

}

これにより、アプリからYAMAHAのAVアンプをコントロールすることが出来ました。

YAMAHAのAVアンプを探す

YAMAHAのアプリでは、起動時にネットワーク内のAVアンプを探しに行っているようでした。これと同じ事ができないかと思い、YAMAHAのアプリ起動時の通信内容を見てみたところ、SSDPプロトコルのブロードキャスト通信が見つかりました。SSDPとは、UPnPで対応機器を探すためのプロトコルです(参考)。
image.png
調べて見ると、実装自体はさほど難しくないですが、面倒なので外部のライブラリを利用します。今回利用したのはssdp-clientというライブラリになります。サイズが小さく、目的の機能を最小限の実装で実現できそうだったからです。

resourcepool/ssdp-client: A simple asynchronous Java SSDP (Simple Service Discovery Protocol) Client

実装としては以下のような感じになりました。

        val client: SsdpClient = SsdpClient.create()
        val networkStorageDevice = SsdpRequest.builder()
            .serviceType("urn:schemas-upnp-org:device:MediaRenderer:1")
            .build()
        client.discoverServices(networkStorageDevice, object : DiscoveryListener {
            override fun onFailed(e: Exception) {
            }

            override fun onServiceDiscovered(service: SsdpService) {
                val names = service.originalResponse.headers["X-MODELNAME"]?.split(":") ?: return
                val name = names.getOrNull(2) ?: names.getOrNull(0) ?: return
                val address = service.remoteIp.hostAddress
                // 取得した情報で何らかの処理を行う
                // ワーカースレッドなので、UI操作を行う場合は注意
            }

            override fun onServiceAnnouncement(announcement: SsdpServiceAnnouncement) {
            }
        })

YAMAHAの機器探索に使用するためのサービスタイプは"urn:schemas-upnp-org:device:MediaRenderer:1"となり、これはキャプチャしたパケットを見ると分かります。(なお、これはDLNAで用いられる汎用的なサービスタイプの模様なので、厳密にやるなら受信したデータに何らかのフィルタリングをかける必要はあるかと思います。)
機器が見つかると、onServiceDiscoveredで渡されるserviceパラメータでサービス情報が受け取れます。複数機器がある場合は、その回数分受信されるはずです。受信したデータを見ると、service.remoteIp.hostAddressに機器のIPアドレスが、service.originalResponse.headers["X-MODELNAME"]に機器名があるようです(少し加工する必要あり)。

こんな感じで、YAMAHAのAVアンプの情報を取得することができました。

その他

2種類の通信

通信ログを追っていくと、YAMAHAのAVアンプに2種類の通信が存在するらしいことが分かります。
一つは、上記で挙げたXMLによる通信。URLは[AVアンプのアドレス]/YamahaRemoteControl で始まります。
もう一つは、JSONによる通信。URLは[AVアンプのアドレス]/YamahaExtendedControl/v1 で始まります。名前からして、XMLの方は昔からあるベーシックな命令で各機種共通、JSONの方は機種毎に差異のある拡張命令かな、という気がします。詳しいことは分からないですが、恐らく互換性を考慮したやや複雑な通信になっている模様です。

作ったアプリ

今回の調査を踏まえ、自分が作ったAndroidアプリはこちらですが、普通のアプリではないのでソースの参考程度に。

最後に

そんな感じで、YAMAHAのAVアンプのコントロールを行ってみました。今回使用した機器はRX-S601ですが、通信内容的に他の機器でも共通のような気がします。思ったより単純なプロトコルだったので、単一の機能だけを実現させるだけなら、実装はそれほど難しくなかったです。
今回はたまたま持っていたYAMAHAの機器でやってみましたが、他のメーカーの通信がどんな感じなのかは少し気になるところです。

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?