概要
YAMAHAのAVアンプ RX-S601 をAndroidの自作アプリから操作してみたので、その辺りの経緯について書いています。
背景
YAMAHAのAVアンプは、携帯端末から操作できるモバイルアプリが提供されています。
以前から個人でYAMAHAのAVアンプRX-S601を使用していたので、このアプリで操作していました。ただ、携帯端末の音楽をアンプのスピーカーから流そうと思った時に、入力ソースをBluetoothに切り替える必要があり、その都度アプリを開いて操作するのが面倒でした。
そこで、自分で入力ソースを切り替える仕組みが作れないかと思い、Androidアプリ上で実装してみた次第です。
通信内容の調査
とりあえず、APIの仕様がどこかに無いかと思ったのですが、特に公開はされていないようでした。GitHubにYAMAHAのAVアンプを操作するソースがいくつかあったのですが、自分の機種で使えるのか分からず、実際に通信の中身を見た方が早そうだったので、ツールで通信をキャプチャすることにしました。
今回は、tPacketCaptureというアプリでキャプチャしました。保存されたデータはWiresharkで直接見る事ができます。以下は実際のサンプル画像です。(AVアンプのIPアドレスは192.168.0.68に設定されています)
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で対応機器を探すためのプロトコルです(参考)。
調べて見ると、実装自体はさほど難しくないですが、面倒なので外部のライブラリを利用します。今回利用したのは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の機器でやってみましたが、他のメーカーの通信がどんな感じなのかは少し気になるところです。