Я уверен, что многие из читателей Хабра знают, или хотя бы слышали, об аудио-аппаратуре компании Onkyo. Современные сетевые плееры и A/V ресиверы имеют на борту Линукс, а также возможность проводного/беспроводного подключения к сети. Компания Onkyo предоставляет своё фирменное мобильное приложение для удалённого управления подобным устройством — Onkyo Controller. Информации, как это приложение работает, практически нет — есть крохи на форумах, а также несколько проектов на 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

Открыто много чего интересного:

  • 80/tcp понятно — это страница настройки устройства. В моей модели здесь только настройка сети и обновление прошивки. Никакого управления воспроизведением нет. Через него же по динамическим ссылкам вида «http://192.168.1.80/album_art.cgi» можно получить доступ к картинке трека, который в данный момент играет.
  • 4545/tcp — появился после самого последнего обновления прошивки. Nmap про него ничего не знает. При попытке соединения сразу же посылает json с текущим статусом воспроизведения и каждую секунду высылает обновление

    Блок данных со статусом воспроизведения
    {
      "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"
    }


    Как я уже сказал, этот порт появился с последним обновлением. Документации нет от слова совсем. Может оказаться полезным для разработки легковесной панели управления. Но в этом направлении я ещё не копал.
  • 5000/tcp — nmap определяет его как Apple AirTunes. Похоже на правду, так как поддержка этого протокола в документации заявлена.
  • 8008/tcp, 8009/tcp — предназначение непонятно, nmap про них ничего не знает.
  • 8080/tcp — http-proxy, назначение которого в контексте данного плеера не совсем понятно.
  • 8888/tcp — порт протокола Universal Plug and Play (UpnP). Утилитой gupnp-universal-cp из пакета gupnp-tools можно просмотреть его описание:

    Сначала было подумал, что управление в официальном приложение реализовано на базе именно этого протокола. Как оказалось впоследствии, ошибся. Также попробовал несколько UpnP клиентов, как мобильных, так и настольных. Все практически не работоспособны: некоторые команды управления срабатывают, некоторые — нет, причём совершенно хаотически.
  • 10001/tcp — похоже на порт конфигурации SCP, но как использовать, непонятно.
  • 60128/tcp — и, наконец, главный герой этой статьи, порт протокола eISCP. Nmap про него тоже ничего не знает. Приоткроем завесу тайны.

Анализ трафика


Теперь проверим, по какому порту и как именно общается с устройством официальное приложение. Проще всего сделать это на каком-нибудь рутованом Андроиде (но не в эмуляторе, так как официальное приложение требует наличия управляемого устройства в той же локальной подсети). Для этого:

  • Установим Android tcpdump на Андроид-устройстве, где уже установлено приложение Onkyo Controller
  • Заходим на Андроид-устройство через adb как root:

    > adb root && adb shell
    root@fiber-bs1078:/>
    
  • переходим в любой каталог встроенной SD-карты:

    root@fiber-bs1078:/> cd /sdcard/work
    root@fiber-bs1078:/sdcard/work> 
    
  • запускаем tcpdump (с записью в файл)

    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
    
  • Запускаем приложение Onkyo, и оттуда запускаем воспроизведение музыки
  • Когда наберётся несколько сотен пакетов, останавливаем tcpdump по Cttl+C
  • Возвращаемся в терминал, откуда запустили ADB и копируем файл на рабочий компьютер

    root@fiber-bs1078:/sdcard/work> exit
    > adb pull /sdcard/work/onkyo.dump .
    [100%] /sdcard/work/onkyo.dump
    
  • Запустим wireshark и смотрим, что там происходит

    > wireshark onkyo.dump & 
    

И действительно, общение идёт по 60128 порту. Например, приложение посылает запрос на плеер:



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



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

Спецификация сообщений


Клиент (например, мобильное приложение) и устройство обмениваются короткими текстовыми сообщениями. Если в нем присутствуют цифры, то они представлены, как правило, в шестнадцатеричном виде.

Формат сообщения от клиента к устройству очень простой:



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

В зависимости от команды, устройство отвечает либо тем же типом сообщения (если это был запрос какого-либо параметра), либо другим типом или даже комбинацией сообщений (если это была команда на сложное действие типа переключения трека). Формат сообщений, посылаемых устройством на клиента, такой же. Отличие только в последнем байте:



В документе описания протокола содержится больше сотни различных команд, моё устройство поддерживает чуть больше 30 команд из этого документа. То есть как набор команд, так и допустимые параметры зависят от конкретного устройства.

Команды можно сгруппировать в логические группы. В качестве примера, я бы выделил такие:

  1. Общее управление устройством:
    • NDN: имя устройства.
    • UPD: проверка и установка обновления прошивки.
    • PWR: включение/выключение питания.
    • NRI: расширенная информация об устройстве.
    • NTC: команды стандартного пульта дистанционного управления (в т.ч. управление воспроизведением).
    • CAP: команды управления внешним усилителем, подключённым к разъёму RI.
  2. Информация о воспроизводимом треке:
    • NAL: имя альбома.
    • NAT: имя артиста.
    • NTI: название трека.
    • NFI: информация о файле трека (формат, битрейт).
    • NJA: картинка, привязанная к треку (например, эмблема радиостанции, если выбрано интернет-радио).
    • NTM: текущая временная позиция в треке.
    • NTS: статус, разрешена «перемотка» или нет (для интернет-радио, например, не разрешена).
    • NST: управление повтором и случайным воспроизведением.
  3. Навигация по фонотеке и управление ей:

    • SLI: выбор источника (например, USB, сетевые сервисы).
    • NSV: выбор конкретного сетевого сервиса (например интернет-радио, музыкальный сервер). Плейлист на моём устройстве также относится к сетевым сервисам, хоть это и не совсем очевидно с точки зрения пользовательского интерфейса. Причём при отключении питания (выдёргивании из розетки) этот плейлист удаляется!
    • NLT, NLA: навигация по разделам (папкам) фонотеки.
    • PQA, PQR, PQO: управление плейлистом: добавление, удаление, изменение порядка.

Естественно, этот список далеко не полный, я привёл его только ради того, чтобы показать охват и возможности данного протокола.

С точки зрения параметров, все сообщения можно разделить на две группы. В первую группу входит большая часть сообщений. Для этой группы строка параметров содержит данные в буквенном или шестнадцатеричном виде и разбирается побайтно. Например, при переходе в сервис 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 этого сообщения.

ISCP over Ethernet (eISCP)


Сообщения в том виде, как я писал выше, предназначены для передачи по кабелю (RS-232). Старые модели ресиверов оснащались для этого 9-контактным разъёмом RS-232. Когда же вместо этого разъёма стали использовать подключение к сети (проводное или беспроводное), то пришлось завернуть эти сообщения в обёртку для передачи по TCP/IP. Так появился протокол eISCP, где ISCP-сообщение завёрнуто в такой пакет:



Под спойлером код процедуры, которая полностью формирует такой пакет для сообщения с заданным кодом (переменная code), сформированной строкой параметров (переменная parameters) и заданной версией протокола (переменная version). Так как процедура достаточно простая, то, мне кажется, код на Джаве скажет много больше, чем тысячи слов.

Процедура формирования eISCP сообщения
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;
}


Если кому интересно, вот здесь находится мой пример реализации протокола для спецификации версии 1.40. Дам также ссылку на этот репозиторий. В нем реализована библиотека сообщений и утилита командной строки на Питоне, а также есть ссылки на другие аналогичные проекты.

Реализация обмена информацией


Сами сообщения, изначально разработанные для передачи по низкоскоростному кабелю, достаточно маленькие. Более того, ещё и сам плеер достаточно скромен — на фоне огромного объёма статистики, отсылаемой куда-то на сервера условного «Амазона», объём информации, которую плеер добровольно отдаёт клиенту по ISCP, просто мизерный. В спецификации протокола нет ни слова о том, когда и при каких условиях плеер посылает ту или иную информацию. Поэтому здесь пришлось достаточно долго экспериментировать самому, чтобы мобильный клиент всегда имел всю необходимую информацию о текущем состоянии устройства.

В целом, общение с плеером строится по схеме запрос/ответ. Причём в определённых ситуациях одним запросом ограничится не получится. Для моего плеера есть несколько ключевых событий, которые нужно обрабатывать:

  • Установка соединения. В момент соединения, плеер может быть в режиме ожидания или включен, может быть в режиме воспроизведения или на паузе. Также важно сразу же узнать, в каком положении находится переключатель входного канала — на сетевых сервисах или USB.

    Поэтому сразу же после установки соединения имеет смысл отправить запросы PWR (активен или в состоянии ожидания), UPD (есть ли обновление прошивки), NRI (общая информация об устройстве), SLI (положение переключателя входа), NJA (режим передачи картинки трека — по ссылке или потоком). Состояние воспроизведения и текущее положение мой конкретно плеер высылает по собственной инициативе.
  • Начало воспроизведения. В этой ситуации плеер высылает всю информацию о треке. Но при установке соединения, когда плеер уже что-то воспроизводит, плеер не высылает ничего. Кроме того, когда плеер переключает трек, высылается не вся информация.

    Универсальным, хоть и ресурсоёмким решением оказалось отслеживать сообщение NST (состояние воспроизведения), и, если это состояние переключилось на «Play», то сразу отправлять 7 запросов: NAT (исполнитель), NAL (заголовок альбома), NTI (заголовок трека), NFI (информация о файле), NTR (номер трека), NTM (текущее время воспроизведения), NMS (меню трека). Есть особенности в прошивке плеера. Например, при воспроизведении плейлиста плеер ну ни в какую не хочет отдавать номер воспроизводимого трека. Но в целом, можно достаточно подробно узнать текущее состояние воспроизведения.
  • Плеер воспроизводит альбом (или плейлист) и переходит на новый трек. Тут у него начинается какой-то словесный вулкан. Под спойлером я спрятал фрагмент лога входящих сообщений, которые плеер высылает в момент переключения трека. Обратите внимание на временной маркер — весь процесс занимает около 14 секунд!

    Фрагмент лога входящих сообщений при переключении трека
    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"?><response status="ok"><items offset="0" totalitems="11" ><item icontype="M" iconid="2d" title="Happy Boys & Girls" selectable="1" /><item icontype="M" iconid="2d" title="My Oh My" selectable="1" /><item icontype="M" iconid="2d" title="Barbie Girl" selectable="1" /><item icontype="M" iconid="2d" title="Good Morning Sunshine" selectable="1" /><item icontype="M" iconid="2d" title="Doctor Jones" selectable="1" /><item icontype="M" iconid="2d" title="Heat Of The Night" selectable="1" /><item icontype="M" iconid="2d" title="Be A Man" selectable="1" /><item icontype="M" iconid="2d" title="Lollipop (Candyman)" selectable="1" /><item icontype="0" iconid="36" title="Roses Are Red" selectable="1" /><item icontype="M" iconid="2d" title="Turn Back Time" selectable="1" /><item icontype="M" iconid="2d" title="Calling You" selectable="1" /></items></response>]
    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)]
    


    Отсюда видно, что при реализации графического интерфейса нельзя его обновлять сразу же при получении любого сообщения с плеера. Поэтому я пошёл по пути создания класса состояния плеера, который обновляется сразу же при получении сообщений, но вот обновление окошек происходит с задержкой в пару сотен миллисекунд, что сглаживает процесс обновления визуальных элементов.

Пример приложения


В качестве примера дам ссылку на мой репозиторий с Андроид-приложением для дистанционного управления плеером Onkyo NS-6130. Есть шанс, что оно будет также работать с Onkyo NS-6170. Но использовать его с каким-нибудь ресивером Onkyo не получится, так как весь интерфейс приложения заточен именно на воспроизведение и управление фонотекой, чего на ресиверах, как правило, нет. Поэтому у меня нет планов как-нибудь это приложение распространять, здесь я пишу о нём только в качестве примера реализации данного протокола.

Структура приложения простейшая, дизайн минималистический. В наличии всего три вкладки:

  • состояние и управление воспроизведением. Хочу обратить внимание, что сам плеер не поддерживает регулировку громкости. Поэтому кнопки управления громкостью будут работать, только если к плееру при помощи кабеля RI подключен внешний усилитель от Onkyo. Сообщения, которое позволяет проверить наличие такого подключения, к сожалению, нет.


  • навигация по фонотеке и управление плейлистом


  • информация об устройстве



В отличие от фирменного приложения, оно в 10 раз меньше, более отзывчивое, поддерживает альбомную ориентацию экрана и различные темы оформления. Весь спектр именно моих задач оно покрывает полностью, хотя есть, и куда расширяться. Однако, также в отличие от фирменного приложения, оно не универсальное.

Если после прочтения этой статьи кто-то из владельцев Onkyo устройств захочет поэкспериментировать со своим экземпляром, я надеюсь, что этот материал и мой пример приложения снизят порог вхождения в тему.

Спасибо за внимание!

Комментарии (3)


  1. natan555
    28.10.2018 22:30
    +1

    Круто. В свое время для Pioneer'а развлекался похожим, но с ним попроще, протокол описан. Для android есть фирменное приложение (огромное и кривое) и VSXRemote от независимого разработчика. Для windows есть пара uwp-приложений (оба так себе) и AvrPioRemote. Но для себя остановился на паре скриптов для Tasker'а и AutoHotkey. Если кому интересно могу поделится


  1. mkulesh Автор
    28.10.2018 22:56

    Фирменное приложение (я во вступлении дал на него ссылку) тоже огромное, но для моего аппарата работает неплохо. Интерфейс достаточно стильный-модный-молодёжный, но по функциональности у меня к нему претензий не было. Просто из спортивного интереса решил его ковырнуть.
    Кстати, про Pionner. Я, когда ковырял прошивку, несколько раз натыкался в ней на это слово. Например, в прошивке моего плеера есть файл такого содержания:


    {
            "title" : "Product Name",
            "modifiable" : true,
            "value": {
                    "type": "string_",
                    "string_": "2016 Onkyo And Pioneer AVR"
            }
    }

    Спасибо за ссылки на протокол Pioneer'а. Идея, действительно, та же самая, просто команды немного по другому называются.


  1. instalator
    29.10.2018 09:11

    Протокол же описан. Я использую эту либу https://github.com/tillbaks/node-eiscp, управляю из системы "умный дом".