- PVSM.RU - https://www.pvsm.ru -
Я уверен, что многие из читателей Хабра знают, или хотя бы слышали, об аудио-аппаратуре компании Onkyo. Современные сетевые плееры и A/V ресиверы имеют на борту Линукс, а также возможность проводного/беспроводного подключения к сети. Компания Onkyo предоставляет своё фирменное мобильное приложение для удалённого управления подобным устройством — Onkyo Controller [1]. Информации, как это приложение работает, практически нет — есть крохи на форумах, а также несколько проектов на github.

Но можно отыскать в сети описание протокола Integra Serial Communication Protocol over Ethernet (eISCP), который и лежит в основе этого приложения. Протокол интересный. На Хабре ни одной статьи по этому протоколу найти не удалось. С одной стороны, ничего трагичного в этом нет, так как эта проприетарщина нигде, кроме Onkyo, вроде бы и не используется. С другой стороны есть шанс, что найдутся энтузиасты, которые захотят самостоятельно порулить своим плеером или ресивером Onkyo. Также статья может быть интересна тем, кто чисто из теоретического любопытства коллекционирует знания по различным сетевым протоколам. Если заинтересовал, прошу под кат.
Официальной информации по теме статьи мало. Поэтому я буду опираться не только на найденную документацию, так как она описывает исключительно команды протокола, но ничего не говорит об особенностях их использования. Много информации удалось получить как из анализа сетевого трафика с использованием tcpdump/wireshark, так и исследования прошивки устройства. Именно с этого я и начну.
Конкретная модель моего устройства не важна. Скажу только, что это сетевой плеер, похожий на тот, что на картинке для привлечения внимания. Он может проигрывать музыку не только с внешних USB-носителей, но и с музыкального сервера (DLNA), а также поддерживает интернет-радио и потоковые сервисы типа Spotify, Deezer и ещё что-то. Естественно, протокол должен всё это разнообразие поддерживать.
Для того чтобы начать задавать правильные вопросы в поисковике, пришлось сначала понять, что за протокол вообще используется. То есть первый шаг — анализ портов. Итак, устройство в сети, его адрес — 192.168.1.80. Сканируем весь диапазон портов:
> nmap -sS -p0-65535 -T5 192.168.1.80
PORT STATE SERVICE
80/tcp open http
4545/tcp open worldscores
5000/tcp open upnp
8008/tcp open http
8009/tcp open ajp13
8080/tcp open http-proxy
8888/tcp open sun-answerbook
10001/tcp open scp-config
60128/tcp open unknown
Открыто много чего интересного:
{
"data": {
"fireCast": false,
"status": {
"duration": 224893,
"playBytes": 0
},
"error": "",
"matchingMediaRoles": [],
"controls": {
"previous": true,
"next_": true,
"seekBytes": true,
"seekTime": true,
"pause": true,
"seekTrack": true
},
"mediaRoles": {
"title": "",
"asciiTitle": ""
},
"playId": {
"systemMemberId": "Onkyo NS-6130",
"timestamp": 447085
},
"state": "playing",
"trackRoles": {
"mediaData": {
"metaData": {
"artist": "Ottawan",
"album": "Greatest Hits",
"serviceID": "Storage_usb2"
}
},
"title": "Shalala-Song",
"flags": {
"file": true
},
"path": "storage_file_usb2:sda-94DB-FB8F/flac/Disco/Ottawan/Greatest Hits (2007)/05-Shalala-Song.flac",
"optPlayingConentInfo": {
"playingTrackTotal": 17,
"playingTrackNo": 4
},
"icon": "file:///tmp/temp_data_albumArt_3c70a403584dc761cabc88ac0dfbb95c",
"type": "audio"
}
},
"playTime": {
"i64_": 139021,
"type": "i64_"
},
"senderVolume": {},
"senderMute": {},
"sender": "Onkyo-NS-6130-E1EE7F"
}
Как я уже сказал, этот порт появился с последним обновлением. Документации нет от слова совсем. Может оказаться полезным для разработки легковесной панели управления. Но в этом направлении я ещё не копал.

Теперь проверим, по какому порту и как именно общается с устройством официальное приложение. Проще всего сделать это на каком-нибудь рутованом Андроиде (но не в эмуляторе, так как официальное приложение требует наличия управляемого устройства в той же локальной подсети). Для этого:
> adb root && adb shell
root@fiber-bs1078:/>
root@fiber-bs1078:/> cd /sdcard/work
root@fiber-bs1078:/sdcard/work>
root@fiber-bs1078:/sdcard/work> tcpdump -vX -i any -w onkyo.dump host 192.168.1.80
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
root@fiber-bs1078:/sdcard/work> exit
> adb pull /sdcard/work/onkyo.dump .
[100%] /sdcard/work/onkyo.dump
> wireshark onkyo.dump &
И действительно, общение идёт по 60128 порту. Например, приложение посылает запрос на плеер:

А тот на него отвечает:

Вот мы и подошли к самой сути статьи, а именно: что же это на картинках выше за буквы такие — ISCP? Эта аббревиатура означает Integra Serial Control Protocol, изначально разработанный для управления устройствами Onkyo через порт RS-232 (есть старая интересная статья [6] по этому поводу). Позже его расширили добавлением префикса «е» и получилось eISCP — Integra Serial Communication Protocol over Ethernet. Обе версии протокола описаны в документе «Technical Documentation: Integrated Serial Communication Protocol for AV Receiver». Первая версия документа датирована 31 октября 2012 года, последняя, которую удалось найти — 4 сентября 2017. Все версии, которые я нашёл, собраны в моём репозитории демонстрационного проекта [7], о котором я расскажу попозже. Дальнейшее изложение будет базироваться как на этом документе, так и на опытах с моим плеером (который, правда, в этом документе явно не упоминается).
Клиент (например, мобильное приложение) и устройство обмениваются короткими текстовыми сообщениями. Если в нем присутствуют цифры, то они представлены, как правило, в шестнадцатеричном виде.
Формат сообщения от клиента к устройству очень простой:

Сообщение начинается с символа «!», потом идёт код целевого устройства, после чего три буквы команды, и затем строка параметров произвольной длины. Оканчивается символом CR (0x0D) или LF (0x0A), или комбинацией CR+LF.
В зависимости от команды, устройство отвечает либо тем же типом сообщения (если это был запрос какого-либо параметра), либо другим типом или даже комбинацией сообщений (если это была команда на сложное действие типа переключения трека). Формат сообщений, посылаемых устройством на клиента, такой же. Отличие только в последнем байте:

В документе описания протокола содержится больше сотни различных команд, моё устройство поддерживает чуть больше 30 команд из этого документа. То есть как набор команд, так и допустимые параметры зависят от конкретного устройства.
Команды можно сгруппировать в логические группы. В качестве примера, я бы выделил такие:
Естественно, этот список далеко не полный, я привёл его только ради того, чтобы показать охват и возможности данного протокола.
С точки зрения параметров, все сообщения можно разделить на две группы. В первую группу входит большая часть сообщений. Для этой группы строка параметров содержит данные в буквенном или шестнадцатеричном виде и разбирается побайтно. Например, при переходе в сервис TuneIn Radio плеер высылает сообщение NLT — информацию о заголовке текущего списка с параметром «0E01000000090100FF0E00TuneIn Radio», который, будучи декодирован в соответствии со спецификацией, даёт такую информацию:
SERVICE=TUNEIN_RADIO; UI=LIST; LAYER=SERVICE_TOP; CURSOR=0; ITEMS=9; LAYERS=1; START=NOT_FIRST; LEFT_ICON=NONE; RIGHT_ICON=TUNEIN_RADIO; STATUS=NONE; title=TuneIn Radio
Практически все сообщения имеют параметр «QSTN», например «!1NLTQSTN». Этот запрос означает просьбу к плееру вернуть актуальную статусную информацию, соответствующую этому типу сообщений. Работает практически всегда, но есть редкие исключения, когда плеер, в зависимости от своего внутреннего настроения, игнорирует такие запросы.
Вторая группа — это сообщения, где параметром является XML, который нужно разбирать с использованием XML-парсера. Из примера выше, находясь с разделе TuneIn Radio, можно послать запрос NLA, на который ответом придёт информация об активном списке в формате XML:
<?xml version="1.0" encoding="utf-8"?>
<response status="ok">
<items offset="0" totalitems="9">
<item icontype="F" iconid="29" title="My Presets" selectable="1" />
<item icontype="F" iconid="29" title="Local Radio" selectable="1" />
<item icontype="F" iconid="29" title="Music" selectable="1" />
<item icontype="F" iconid="29" title="Talk" selectable="1" />
<item icontype="F" iconid="29" title="Sports" selectable="1" />
<item icontype="F" iconid="29" title="By Location" selectable="1" />
<item icontype="F" iconid="29" title="By Language" selectable="1" />
<item icontype="F" iconid="29" title="Podcasts" selectable="1" />
<item icontype="F" iconid="29" title="Login" selectable="1" />
</items>
</response>
То есть плеер не только предоставляет текстовую информацию (которая, кстати, отображается в данный момент на дисплее самого плеера), но также и советует адекватную иконку (папка, музыкальный трек, проигрываемый в данный момент трек).
В некоторых случаях плеер хочет показать в клентском приложении текстовое сообщение или запросить дополнительные параметры типа имени пользователя. Для этого плеер посылает универсальное сообщение NCP (универсальный диалог), где в XML описана структура того, что нужно показать пользователю:
<?xml version="1.0" encoding="utf-8"?>
<popup title="Try Deezer Premium+" align="center" type="custom" time="0" uri="resource:///popup">
<label title="" align="center" total="1" uri="resource:///popup/label:0">
<line text="Listening is limited to 30-second clips. Subscribe to enjoy unlimited music!"
align="left" uri="resource:///popup/label/line:0" order="0" />
</label>
<buttongroup title="" align="center" total="1" uri="resource:///popup/buttongroup:0">
<button text="OK"
align="center" uri="/button:0" selected="false" index="0" www="" order="1" />
</buttongroup>
</popup>
В ответ плеер ожидает это же самое сообщение с заполненными полями (или нажатой кнопкой).
Также в XML формате представлено достаточно важное сообщение NRI — общая информация о плеере. Сообщение достаточно большое, поэтому прячу его под спойлер.
<?xml version="1.0" encoding="utf-8"?>
<response status="ok">
<device id="NS-6130">
<brand>ONKYO</brand>
<category>NAP-O</category>
<year>2016</year>
<model>NS-6130</model>
<destination>xx</destination>
<macaddress>0009B0E1EE7F</macaddress>
<modeliconurl>http://192.168.1.80/icon/OAVR_120.jpg</modeliconurl>
<friendlyname></friendlyname>
<firmwareversion>2110-0000-0000-0010-0000</firmwareversion>
<ecosystemversion>200</ecosystemversion>
<netservicelist count="9">
<netservice id="0e" value="1" name="TuneIn Radio" account="Username" password="Password"
zone="01" enable="01" />
<netservice id="0a" value="1" name="Spotify" zone="01" enable="01" />
<netservice id="12" value="1" name="Deezer" account="Email address" password="Password"
zone="01" enable="01" />
<netservice id="18" value="1" name="AirPlay" zone="01" enable="01" />
<netservice id="1b" value="1" name="TIDAL" account="Username" password="Password"
zone="01" enable="01" />
<netservice id="00" value="1" name="Music Server" zone="01" enable="01" addqueue="1"
sort="1" />
<netservice id="43" value="1" name="FlareConnect" zone="07" enable="0e" />
<netservice id="40" value="1" name="Chromecast built-in" zone="01" enable="01" />
<netservice id="1d" value="1" name="Play Queue" zone="01" enable="01" />
</netservicelist>
<zonelist count="4">
<zone id="1" value="1" name="Main" volmax="0" volstep="0" src="1" dst="1"
lrselect="0" />
<zone id="2" value="0" name="Zone2" volmax="0" volstep="0" src="0" dst="0"
lrselect="0" />
<zone id="3" value="0" name="Zone3" volmax="0" volstep="0" src="0" dst="0"
lrselect="0" />
<zone id="4" value="0" name="Zone4" volmax="0" volstep="0" src="0" dst="0"
lrselect="0" />
</zonelist>
<selectorlist count="3">
<selector id="2b" value="1" name="NET" zone="01" iconid="2b" />
<selector id="29" value="1" name="USB(F)" zone="01" iconid="29" addqueue="1" />
<selector id="2a" value="1" name="USB(R)" zone="01" iconid="2a" addqueue="1" />
</selectorlist>
<presetlist count="40">
<preset id="01" band="0" freq="0" name="" />
<preset id="02" band="0" freq="0" name="" />
<preset id="03" band="0" freq="0" name="" />
<preset id="04" band="0" freq="0" name="" />
<preset id="05" band="0" freq="0" name="" />
<preset id="06" band="0" freq="0" name="" />
<preset id="07" band="0" freq="0" name="" />
<preset id="08" band="0" freq="0" name="" />
<preset id="09" band="0" freq="0" name="" />
<preset id="0a" band="0" freq="0" name="" />
<preset id="0b" band="0" freq="0" name="" />
<preset id="0c" band="0" freq="0" name="" />
<preset id="0d" band="0" freq="0" name="" />
<preset id="0e" band="0" freq="0" name="" />
<preset id="0f" band="0" freq="0" name="" />
<preset id="10" band="0" freq="0" name="" />
<preset id="11" band="0" freq="0" name="" />
<preset id="12" band="0" freq="0" name="" />
<preset id="13" band="0" freq="0" name="" />
<preset id="14" band="0" freq="0" name="" />
<preset id="15" band="0" freq="0" name="" />
<preset id="16" band="0" freq="0" name="" />
<preset id="17" band="0" freq="0" name="" />
<preset id="18" band="0" freq="0" name="" />
<preset id="19" band="0" freq="0" name="" />
<preset id="1a" band="0" freq="0" name="" />
<preset id="1b" band="0" freq="0" name="" />
<preset id="1c" band="0" freq="0" name="" />
<preset id="1d" band="0" freq="0" name="" />
<preset id="1e" band="0" freq="0" name="" />
<preset id="1f" band="0" freq="0" name="" />
<preset id="20" band="0" freq="0" name="" />
<preset id="21" band="0" freq="0" name="" />
<preset id="22" band="0" freq="0" name="" />
<preset id="23" band="0" freq="0" name="" />
<preset id="24" band="0" freq="0" name="" />
<preset id="25" band="0" freq="0" name="" />
<preset id="26" band="0" freq="0" name="" />
<preset id="27" band="0" freq="0" name="" />
<preset id="28" band="0" freq="0" name="" />
</presetlist>
<controllist count="61">
<control id="Bass" value="0" zone="1" min="-10" max="10" step="2" />
<control id="Treble" value="0" zone="1" min="-10" max="10" step="2" />
<control id="Center Level" value="0" zone="1" min="-12" max="12" step="1" />
<control id="Subwoofer Level" value="0" zone="1" min="-15" max="12" step="1" />
<control id="Subwoofer1 Level" value="0" zone="1" min="-15" max="12" step="1" />
<control id="Subwoofer2 Level" value="0" zone="1" min="-15" max="12" step="1" />
<control id="Phase Matching Bass" value="0" />
<control id="LMD Movie/TV" value="0" code="MOVIE" position="1" />
<control id="LMD Music" value="0" code="MUSIC" position="2" />
<control id="LMD Game" value="0" code="GAME" position="3" />
<control id="LMD THX" value="0" code="04" position="4" />
<control id="LMD Stereo" value="0" code="00" position="4" />
<control id="LMD Direct" value="0" code="01" position="1" />
<control id="LMD Pure Audio" value="0" code="11" position="2" />
<control id="LMD Pure Direct" value="0" code="11" position="1" />
<control id="LMD Auto/Direct" value="0" code="AUTO" position="2" />
<control id="LMD Stereo G" value="0" code="STEREO" position="3" />
<control id="LMD Surround" value="0" code="SURR" position="4" />
<control id="TUNER Control" value="0" />
<control id="TUNER Freq Control" value="0" />
<control id="Info" value="2" />
<control id="Cursor" value="1" />
<control id="Home" value="0" code="HOME" position="2" />
<control id="Setup" value="1" code="MENU" position="2" />
<control id="Quick" value="0" code="QUICK" position="1" />
<control id="Menu" value="0" code="MENU" position="1" />
<control id="AMP Control(RI)" value="1" />
<control id="CD Control(RI)" value="1" />
<control id="CD Control" value="0" />
<control id="BD Control(CEC)" value="0" />
<control id="TV Control(CEC)" value="0" />
<control id="NoPowerButton" value="0" />
<control id="DownSample" value="0" />
<control id="Dimmer" value="1" />
<control id="time_hhmmss" value="1" />
<control id="Zone2 Control(CEC)" value="0" />
<control id="Sub Control(CEC)" value="0" />
<control id="NoNetworkStandby" value="0" />
<control id="NJAREQ" value="1" />
<control id="Music Optimizer" value="0" />
<control id="NoVideoInfo" value="1" />
<control id="NoAudioInfo" value="1" />
<control id="AV Adjust" value="0" />
<control id="Audio Scalar" value="0" />
<control id="Hi-Bit" value="0" />
<control id="Upsampling" value="0" />
<control id="Digital Filter" value="1" />
<control id="DolbyAtmos" value="0" />
<control id="DTS:X" value="0" />
<control id="MCACC" value="0" />
<control id="Dialog Enhance" value="0" />
<control id="PQLS" value="0" />
<control id="CD Control(NewRemote)" value="0" />
<control id="NoVolume" value="1" />
<control id="Auto Sound Retriever" value="0" />
<control id="Lock Range Adjust" value="0" />
<control id="P.BASS" value="0" />
<control id="Tone Direct" value="0" />
<control id="DetailedFileInfo" value="1" />
<control id="NoDABPresetFunc" value="0" />
<control id="S.BASS" value="0" />
</controllist>
<functionlist count="10">
<function id="UsbUpdate" value="0" />
<function id="NetUpdate" value="1" />
<function id="WebSetup" value="1" />
<function id="WifiSetup" value="1" />
<function id="Nettune" value="0" />
<function id="Initialize" value="0" />
<function id="Battery" value="0" />
<function id="AutoStandbySetting" value="0" />
<function id="e-onkyo" value="0" />
<function id="UsbDabDongle" value="0" />
</functionlist>
<tuners count="0"></tuners>
</device>
</response>
Набор команд, который придётся задействовать для управления устройством, во многом зависит от того, что находится с секциях zonelist и controllist этого сообщения.
Сообщения в том виде, как я писал выше, предназначены для передачи по кабелю (RS-232). Старые модели ресиверов оснащались для этого 9-контактным разъёмом RS-232. Когда же вместо этого разъёма стали использовать подключение к сети (проводное или беспроводное), то пришлось завернуть эти сообщения в обёртку для передачи по TCP/IP. Так появился протокол eISCP, где ISCP-сообщение завёрнуто в такой пакет:

Под спойлером код процедуры, которая полностью формирует такой пакет для сообщения с заданным кодом (переменная code), сформированной строкой параметров (переменная parameters) и заданной версией протокола (переменная version). Так как процедура достаточно простая, то, мне кажется, код на Джаве скажет много больше, чем тысячи слов.
private final static int MIN_MSG_LENGTH = 22;
private final static String MSG_START = "ISCP";
private final static Character START_CHAR = '!';
private final static int LF = 0x0A;
...
byte[] getBytes()
{
if (headerSize + dataSize < MIN_MSG_LENGTH)
{
return null;
}
final byte[] bytes = new byte[headerSize + dataSize];
Arrays.fill(bytes, (byte) 0);
// Message header
for (int i = 0; i < MSG_START.length(); i++)
{
bytes[i] = (byte) MSG_START.charAt(i);
}
// Header size
byte[] size = ByteBuffer.allocate(4).putInt(headerSize).array();
System.arraycopy(size, 0, bytes, 4, size.length);
// Data size
size = ByteBuffer.allocate(4).putInt(dataSize).array();
System.arraycopy(size, 0, bytes, 8, size.length);
// Version
bytes[12] = (byte) version;
// CMD
bytes[16] = (byte) START_CHAR.charValue();
bytes[17] = (byte) '1';
for (int i = 0; i < code.length(); i++)
{
bytes[i + 18] = (byte) code.charAt(i);
}
// Parameters
for (int i = 0; i < parameters.length(); i++)
{
bytes[i + 21] = (byte) parameters.charAt(i);
}
// End char
bytes[21 + parameters.length()] = (byte) LF;
return bytes;
}
Если кому интересно, вот здесь [8] находится мой пример реализации протокола для спецификации версии 1.40 [9]. Дам также ссылку на этот репозиторий [10]. В нем реализована библиотека сообщений и утилита командной строки на Питоне, а также есть ссылки на другие аналогичные проекты.
Сами сообщения, изначально разработанные для передачи по низкоскоростному кабелю, достаточно маленькие. Более того, ещё и сам плеер достаточно скромен — на фоне огромного объёма статистики, отсылаемой куда-то на сервера условного «Амазона», объём информации, которую плеер добровольно отдаёт клиенту по ISCP, просто мизерный. В спецификации протокола нет ни слова о том, когда и при каких условиях плеер посылает ту или иную информацию. Поэтому здесь пришлось достаточно долго экспериментировать самому, чтобы мобильный клиент всегда имел всю необходимую информацию о текущем состоянии устройства.
В целом, общение с плеером строится по схеме запрос/ответ. Причём в определённых ситуациях одним запросом ограничится не получится. Для моего плеера есть несколько ключевых событий, которые нужно обрабатывать:
Поэтому сразу же после установки соединения имеет смысл отправить запросы PWR (активен или в состоянии ожидания), UPD (есть ли обновление прошивки), NRI (общая информация об устройстве), SLI (положение переключателя входа), NJA (режим передачи картинки трека — по ссылке или потоком). Состояние воспроизведения и текущее положение мой конкретно плеер высылает по собственной инициативе.
Универсальным, хоть и ресурсоёмким решением оказалось отслеживать сообщение NST (состояние воспроизведения), и, если это состояние переключилось на «Play», то сразу отправлять 7 запросов: NAT (исполнитель), NAL (заголовок альбома), NTI (заголовок трека), NFI (информация о файле), NTR (номер трека), NTM (текущее время воспроизведения), NMS (меню трека). Есть особенности в прошивке плеера. Например, при воспроизведении плейлиста плеер ну ни в какую не хочет отдавать номер воспроизводимого трека. Но в целом, можно достаточно подробно узнать текущее состояние воспроизведения.
10-27 16:12:20.272: NLU[00080011; 8/17]
10-27 16:12:27.338: NTI[09-Roses Are Red.flac]
10-27 16:12:27.342: NAL[]
10-27 16:12:27.342: NAT[]
10-27 16:12:27.342: NDN[]
10-27 16:12:27.343: NJA/1937[2-...; TYPE=URL; PACKET=NOT_USED; URL=http://192.168.1.80/album_art.cgi; RAW(null)]
10-27 16:12:27.649: NMS[xxxxxS1f1; TRACK_MENU=DISABLE; POS_FEED=DISABLE; NEG_FEED=DISABLE; TIME_SEEK=ENABLE; TIME_DISPLAY=ELAPSED_TOTAL; ICON=USB_REAR]
10-27 16:12:27.649: NTR[0009; 0011]
10-27 16:12:27.649: NFI[/44.1kHz/16bit; FORMAT=; FREQUENCY=44.1kHz; BITRATE=16bit]
10-27 16:12:27.649: NLT[F1020000000B060002FF00Aquarium (1997); SERVICE=USB_REAR; UI=LIST; LAYER=UNDER_2ND_LAYER; CURSOR=0; ITEMS=11; LAYERS=6; START=NOT_FIRST; LEFT_ICON=USB; RIGHT_ICON=NONE; STATUS=NONE; title=Aquarium (1997)]
10-27 16:12:27.724: NLS[C0P; INF_TYPE=CURSOR; LINE_INFO=0; PROPERTY=NO; UPD_TYPE=PAGE; LIST_DATA=null]
10-27 16:12:27.727: NLS[U0-Happy Boys & Girls; INF_TYPE=UNICODE; LINE_INFO=0; PROPERTY=NO; UPD_TYPE=NO; LIST_DATA=Happy Boys & Girls]
10-27 16:12:27.734: NLS[U1-My Oh My; INF_TYPE=UNICODE; LINE_INFO=1; PROPERTY=NO; UPD_TYPE=NO; LIST_DATA=My Oh My]
10-27 16:12:27.737: NLS[U2-Barbie Girl; INF_TYPE=UNICODE; LINE_INFO=2; PROPERTY=NO; UPD_TYPE=NO; LIST_DATA=Barbie Girl]
10-27 16:12:27.740: NLS[U3-Good Morning Sunshine; INF_TYPE=UNICODE; LINE_INFO=3; PROPERTY=NO; UPD_TYPE=NO; LIST_DATA=Good Morning Sunshine]
10-27 16:12:27.760: NLA[X0002S000...; RESP=X; SEQ_NR=2; STATUS=S; UI=LIST; XML=<?xml version="1.0" encoding="utf-8"?>/>/>/>/>/>/>/>/>/>/>/>]
10-27 16:12:29.697: NTI[Roses Are Red]
10-27 16:12:29.718: NJA/1952[2-...; TYPE=URL; PACKET=NOT_USED; URL=http://192.168.1.80/album_art.cgi; RAW(null)]
10-27 16:12:30.248: NAL[Aquarium]
10-27 16:12:30.248: NAT[Aqua]
10-27 16:12:30.248: NDN[]
10-27 16:12:30.248: NMS[xxxxxS1f1; TRACK_MENU=DISABLE; POS_FEED=DISABLE; NEG_FEED=DISABLE; TIME_SEEK=ENABLE; TIME_DISPLAY=ELAPSED_TOTAL; ICON=USB_REAR]
10-27 16:12:30.248: NTR[0009; 0011]
10-27 16:12:30.248: NFI[FLAC/44.1kHz/16bit; FORMAT=FLAC; FREQUENCY=44.1kHz; BITRATE=16bit]
10-27 16:12:30.248: NLT[F1020000000B060002FF00Aquarium (1997); SERVICE=USB_REAR; UI=LIST; LAYER=UNDER_2ND_LAYER; CURSOR=0; ITEMS=11; LAYERS=6; START=NOT_FIRST; LEFT_ICON=USB; RIGHT_ICON=NONE; STATUS=NONE; title=Aquarium (1997)]
10-27 16:12:30.248: NMS[xxxxxS1f1; TRACK_MENU=DISABLE; POS_FEED=DISABLE; NEG_FEED=DISABLE; TIME_SEEK=ENABLE; TIME_DISPLAY=ELAPSED_TOTAL; ICON=USB_REAR]
10-27 16:12:30.248: NFI[FLAC/44.1kHz/16bit; FORMAT=FLAC; FREQUENCY=44.1kHz; BITRATE=16bit]
10-27 16:12:30.248: NLT[F1020000000B060002FF00Aquarium (1997); SERVICE=USB_REAR; UI=LIST; LAYER=UNDER_2ND_LAYER; CURSOR=0; ITEMS=11; LAYERS=6; START=NOT_FIRST; LEFT_ICON=USB; RIGHT_ICON=NONE; STATUS=NONE; title=Aquarium (1997)]
10-27 16:12:34.815: NMS[xxxxxS1f1; TRACK_MENU=DISABLE; POS_FEED=DISABLE; NEG_FEED=DISABLE; TIME_SEEK=ENABLE; TIME_DISPLAY=ELAPSED_TOTAL; ICON=USB_REAR]
10-27 16:12:34.819: NFI[FLAC/44.1kHz/16bit; FORMAT=FLAC; FREQUENCY=44.1kHz; BITRATE=16bit]
10-27 16:12:34.860: NLT[F1020000000B060002FF00Aquarium (1997); SERVICE=USB_REAR; UI=LIST; LAYER=UNDER_2ND_LAYER; CURSOR=0; ITEMS=11; LAYERS=6; START=NOT_FIRST; LEFT_ICON=USB; RIGHT_ICON=NONE; STATUS=NONE; title=Aquarium (1997)]
Отсюда видно, что при реализации графического интерфейса нельзя его обновлять сразу же при получении любого сообщения с плеера. Поэтому я пошёл по пути создания класса состояния плеера, который обновляется сразу же при получении сообщений, но вот обновление окошек происходит с задержкой в пару сотен миллисекунд, что сглаживает процесс обновления визуальных элементов.
В качестве примера дам ссылку на мой репозиторий [11] с Андроид-приложением для дистанционного управления плеером Onkyo NS-6130. Есть шанс, что оно будет также работать с Onkyo NS-6170. Но использовать его с каким-нибудь ресивером Onkyo не получится, так как весь интерфейс приложения заточен именно на воспроизведение и управление фонотекой, чего на ресиверах, как правило, нет. Поэтому у меня нет планов как-нибудь это приложение распространять, здесь я пишу о нём только в качестве примера реализации данного протокола.
Структура приложения простейшая, дизайн минималистический. В наличии всего три вкладки:
В отличие от фирменного приложения, оно в 10 раз меньше, более отзывчивое, поддерживает альбомную ориентацию экрана и различные темы оформления. Весь спектр именно моих задач оно покрывает полностью, хотя есть, и куда расширяться. Однако, также в отличие от фирменного приложения, оно не универсальное.
Если после прочтения этой статьи кто-то из владельцев Onkyo устройств захочет поэкспериментировать со своим экземпляром, я надеюсь, что этот материал и мой пример приложения снизят порог вхождения в тему.
Спасибо за внимание!
Автор: Михаил Кулеш
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/297263
Ссылки в тексте:
[1] Onkyo Controller: https://play.google.com/store/apps/details?id=com.onkyo.jp.onkyocontroller
[2] Universal Plug and Play (UpnP): https://en.wikipedia.org/wiki/Universal_Plug_and_Play
[3] Android tcpdump: https://www.androidtcpdump.com
[4] adb: https://developer.android.com/studio/command-line/adb
[5] wireshark: https://www.wireshark.org
[6] статья: http://robotskirts.com/2012/04/28/controlling-onkyo-integra-receivers-via-rs-232
[7] моём репозитории демонстрационного проекта: https://github.com/mkulesh/onpc/tree/master/doc
[8] здесь: https://github.com/mkulesh/onpc/tree/master/app/src/main/java/com/mkulesh/onpc/iscp
[9] спецификации версии 1.40: https://github.com/mkulesh/onpc/blob/master/doc/ISCP_AVR_140.xlsx
[10] этот репозиторий: https://github.com/miracle2k/onkyo-eiscp
[11] мой репозиторий: https://github.com/mkulesh/onpc
[12] Источник: https://habr.com/post/427985/?utm_source=habrahabr&utm_medium=rss&utm_campaign=427985
Нажмите здесь для печати.