Протокол ISCP/eISCP от Onkyo: управление устройствами Onkyo по сети

  • Tutorial
Я уверен, что многие из читателей Хабра знают, или хотя бы слышали, об аудио-аппаратуре компании 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 устройств захочет поэкспериментировать со своим экземпляром, я надеюсь, что этот материал и мой пример приложения снизят порог вхождения в тему.

Спасибо за внимание!
  • +18
  • 1,9k
  • 9
Поделиться публикацией

Похожие публикации

Комментарии 9

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

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


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

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

        0
        Я, когда ковырял прошивку.

        А чем (как) распаковывали прошивку?

          0

          Это нетривиально. Три шага:


          1. Сама прошивка зашифрована. Но есть программа-дешифратор. Неплохое руководство есть здесь. Важная особенность — дешифратор нужно компилировать под x86, а не x86_64:
            gcc -o onkyo-dec -m32 onkyo-decrypt.c
          2. После дешифровки получаете несколько файлов, один из которых — образ Линукса (UBI image, version 1).
            of2.AM335XNA_010203040506.07299: UBI image, version 1
            Самый простой путь — распаковать его этой программкой:
            python ubidump.py --savedir ubi of2.AM335XNA_010203040506.07299
            После этого в папке ubi получите все дерево rootfs. Этот rootfs — стандартный Линукс
          3. Но в папке rootfs/home/root/ лежат еще два файла: system.img и usr.img. Один и них — Google Chome, а вот второй — сам софт плеера. Самое простое — подмонтировать их как squashfs:
            mount -t squashfs -o loop home/root/usr.img ./usr
            mount -t squashfs -o loop home/root/system.img ./system
            После этого можно изучать. Не ручаюсь, что эта инструкция будет верна для других устройств Onkyo.
            +1

            Спасибо, поковыряем


            Для желающих - подправленная версия декриптора, которая нормально собирается и под 32 и под 64 bit
            /*
             *  Onkyo firmware decryptor v2 (c) 2014 - vZ@divideoverflow.com
             * 
             *  version 2:
             *  re-written for more sophisticated parsing, fixing bug with some blocks being missed
             * 
             *  version 1.0:
             *  initial release
             * 
             *  Thanks to Turmio for the only page found on the web dedicated to ONKYO reversing
             *  (https://jkry.org/ouluhack/HackingOnkyo%20TR-NX509)
             *  and to Na Na for providing libupdater.so.
             * 
             * 
             */
            
            #include <stdio.h>
            #include <string.h>
            #include <sys/types.h>
            #include <sys/stat.h>
            #include <glob.h>
            #include <fcntl.h>
            #include <unistd.h>
            #include <sys/mman.h>
            #include <errno.h>
            
            #include <stdlib.h> // atoi
            
            #include <stdint.h>
            // these keys were found statically entered in libupdater.so
            // however, they aren't usable for every block, hence we'll calculate most of them on demand
            // using known-plaintext attack.
            uint8_t keyA[8] = "\xda\x57\x68\x0d\x44\x21\x30\x7a";
            uint8_t keyB[8] = "\xae\xb7\x31\x74\x47\xe4\xfb\x5d";
            
            uint8_t cryptKey[8] = { 0 };
            
            char plaintext[] = "ONKYO Encryption";
            uint32_t Magic1 = 0x57cb4295;
            
            FILE *fp;
            char path[4000] = { 0 };
            char outname[4000] = { 0 };
            char outdir[4000] = { 0 };
            
            uint32_t blocksize = 0x1000;
            uint8_t lastkey = 0;
            uint32_t counter = 0;
            uint8_t dst[0x1000] = { 0 };
            
            int32_t ofnum = 0;
            
            uint32_t
            calc_crc (uint8_t *src, uint32_t size)
            {
              uint32_t x = 0, y = 0;
              int32_t i = 0;
              uint8_t b1, b2, b3, b4;
            
              size--;
            
              do
                {
                  b1 = src[i++];
                  x = y + b1;
                  if (i > size)
                break;
                  b2 = src[i++];
                  x += b2 << 8;
                  if (i > size)
                break;
                  b3 = src[i++];
                  x += b3 << 16;
                  if (i > size)
                break;
                  b4 = src[i++];
                  x += b4 << 24;
                  y = (x << 11) + (x >> 21);
                  if (i > size)
                break;
            
                }
              while (1);
            
              return x;
            }
            
            void
            calc_key (uint8_t *src, uint8_t *cryptKey)
            {
              uint8_t key[8] = { 0 };
              int32_t n, j, b;
              uint8_t lk = 0;
            
              lk = plaintext[0] ^ src[0];
              key[0] = lk;
            
              for (j = 1; j < 8; j++)
                {
                  n = lk >> 7;
                  b = n;
                  lk = lk & 0x7f;
                  n = n | (lk << 1);
                  lk = plaintext[j] ^ src[j];
                  key[j] = lk + 0x100 * b - n;
                }
            
              memcpy (cryptKey, key, 8);
            }
            
            int32_t
            match_crc (uint8_t *src, uint32_t size, uint32_t crc)
            {
              return (crc == calc_crc (src, size));
            }
            
            void
            decrypt_block (uint8_t *src, uint8_t *dst, int32_t size,
                       uint8_t *xorkey, uint32_t *c, uint8_t *lk)
            {
              int32_t n, i = 0, j = 0;
              int32_t k = 0;
            
              if (size == 0)
                return;
            
              if (*c == 0)
                *lk = xorkey[0];
              j = *c % 7;
            
              do
                {
            
                  dst[i] = src[i] ^ *lk;
                  n = (*lk >> 7);
                  j++;
                  k = xorkey[j];
                  *lk = *lk & 0x7f;
                  n = n | (*lk << 1);
                  k = k + n;
                  *c = *c + 1;
                  k = k + (*c >> 6);
                  *lk = k & 0xff;
                  if (j == 7)
                j = 0;
                  i++;
                  size--;
            
                }
              while (size > 0);
            
              return;
            }
            
            void
            make_target_dir ()
            {
              struct stat sb;
              int32_t e;
            
              strcpy (outdir, path);
              strcat (outdir, "/extracted");
            
              e = stat (outdir, &sb);
              if (e == 0)
                {
                  if (!(sb.st_mode & S_IFDIR))
                {
                  fprintf (stdout, "Target '%s' must be a directory!\n", outdir);
                }
                }
              else
                {
                  if (errno = ENOENT)
                {
                  e = mkdir (outdir, S_IRWXU);
                  if (e != 0)
                    perror ("mkdir failed\n");
                }
                }
            }
            
            int32_t
            parse_header (uint8_t *src)
            {
            
              typedef struct header
              {
                char sig[0x10];
                uint32_t dataofs;
                uint32_t crc;
                uint32_t pname;
                uint32_t ptree;
                uint32_t precords;
                uint8_t unk1[12];
                char name[0x20];
                char subname[4];
                uint8_t unpackedfiles;
                uint8_t packedfiles;
                uint8_t ofnum;
                uint8_t fileshere;
                uint8_t unk2[0x1a8];
              } __attribute__((packed)) t_header;
              t_header hdr;
            
              typedef struct block
              {
                char filename[8];
                uint32_t offset;
                uint32_t size;
                uint32_t crc;
              } __attribute__((packed)) t_block;
              t_block blk[20];
            
              uint8_t *blkptr;
              int32_t result = -1;
              int32_t tb = 0;
              int32_t i, f;
              uint32_t counter = 0;
            
              decrypt_block (src, dst, sizeof (hdr), keyA, &counter, &lastkey);
              if (memcmp (dst, plaintext, 0x10) != 0)
                return -1;
              memcpy (&hdr, dst, sizeof (hdr));
            
              if (match_crc
                  ((uint8_t *) (&hdr) + 0x18, hdr.dataofs - 0x18, hdr.crc))
                {
                  sprintf (outname, "%s/of%i.%s.hdr", outdir, ofnum, hdr.name);
                  fp = fopen (outname, "w");
                  if (fp)
                {
                  fwrite (&hdr, 1, hdr.dataofs, fp);
                  fclose (fp);
                  fprintf (stdout, "Header block decrypted and saved to %s\n",
                       outname);
                  result = 1;
                }
            
                  int32_t prec = hdr.precords;
                  int32_t frec = hdr.ptree;
                  while (prec < hdr.dataofs)
                {
                  if (*(uint32_t *) ((uint8_t *) &hdr + prec) != 0
                      && *(uint32_t *) ((uint8_t *) &hdr + prec + 4))
                    {
                      blk[tb].size =
                    *(uint32_t *) ((uint8_t *) &hdr + prec);
                      blk[tb].offset =
                    *(uint32_t *) ((uint8_t *) &hdr + prec + 4);
                      blk[tb].crc =
                    *(uint32_t *) ((uint8_t *) &hdr + prec + 8);
                      strncpy (blk[tb].filename, (char *) ((char *) &hdr + frec + 1),
                           7);
                      tb++;
                    }
                  prec += 0x10;
                  frec += 8;
                }
            
                  for (i = 0; i < tb; i++)
                {
            
                  blkptr = src + blk[i].offset;
                  counter = 0;
                  blocksize = 0x1000;
            
                  if (calc_crc (blkptr, blk[i].size) != blk[i].crc)
                    {
                      fprintf (stdout, "Error: CRC mismatch! Skipping block..\n");
                      continue;
                    }
            
                  if (*(uint32_t *) (blkptr) == Magic1)
                    {
                      // process header
                      if (parse_header (blkptr) < 0)
                    fprintf (stdout, "Error parsing header block!\n");
                    }
                  else
                    {
            
                      // calculate decryption key
                      calc_key (blkptr, cryptKey);
            
                      sprintf (outname, "%s/of%i.%s.%s", outdir, ofnum, hdr.name,
                           blk[i].filename);
                      fprintf (stdout,
                           "Writing block from 0x%.8x of size %u to %s\n",
                           blk[i].offset, blk[i].size, outname);
            
                      f = 0;
            
                      do
                    {
                      decrypt_block (blkptr, dst, blocksize, cryptKey, &counter,
                             &lastkey);
                      if (counter == blocksize)
                        {
                          // verify we got it right
                          if (memcmp (dst, plaintext, 0x10) != 0)
                        {
                          fprintf (stdout,
                               "Error: Invalid decryption key/signature .. skipping this block.\n\n");
                          break;
                        }
            
                          f = 1;
                          fp = fopen (outname, "w");
                          fwrite (dst + 0x10, 1, blocksize - 0x10, fp);
            
                        }
                      else
                        {
                          fwrite (dst, 1, blocksize, fp);
                        }
            
                      blkptr += blocksize;
            
                      if (counter == blk[i].size)
                        break;
                      if (blk[i].size - counter < blocksize)
                        blocksize = blk[i].size - counter;
            
                    }
                      while (1);
            
                      if (f)
                    {
                      fclose (fp);
                      fprintf (stdout,
                           "Block successfully decrypted and saved.\n");
                      result = 1;
                    }
                    }
                }
            
                }
              else
                {
                  fprintf (stdout, "Error: Header CRC mismatch.. skipping.\n");
                }
            
              return result;
            }
            
            int32_t
            main (int argc, char *argv[])
            {
              int32_t fd;
              glob_t globbuf;
              struct stat sb;
              uint8_t *p;
              char searchpath[0x4000] = { 0 };
              long long buflen;
              int32_t j;
            
              fprintf (stdout,
                   "Decrypt Onkyo firmware, (c) 2014, <vZ@divideoverflow.com>\n\n");
            
              if (argc > 1)
                {
                  strcpy (path, argv[1]);
                }
              else
                {
                  strcpy (path, ".");
                }
            
              fprintf (stdout, "Searching for firmware '.of' files in '%s' .. ", path);
            
              strcpy (searchpath, path);
              strcat (searchpath, "/*.of?");
              glob (searchpath, 0, NULL, &globbuf);
            
              if (globbuf.gl_pathc > 0)
                {
                  fprintf (stdout, "%zi files found.\n", globbuf.gl_pathc);
                }
              else
                {
                  fprintf (stdout, "no files found.\n");
                  return 0;
                }
            
              make_target_dir ();
            
              for (j = 0; j < globbuf.gl_pathc; j++)
                {
            
                  fprintf (stdout, "\nProcessing %s..\n", globbuf.gl_pathv[j]);
            
                  ofnum = atoi (globbuf.gl_pathv[j] + strlen (globbuf.gl_pathv[j]) - 1);
            
                  fd = open (globbuf.gl_pathv[j], O_RDWR);
                  if (fd == -1)
                {
                  perror ("open");
                  return 1;
                }
                  if (fstat (fd, &sb) == -1)
                {
                  perror ("fstat");
                  return 1;
                }
                  if (!S_ISREG (sb.st_mode))
                {
                  fprintf (stdout, "%s is not a file\n", globbuf.gl_pathv[j]);
                  return 1;
                }
            
                  buflen = sb.st_size;
                  p = mmap (0, buflen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
            
                  if (p == MAP_FAILED)
                {
                  perror ("mmap");
                  return 1;
                }
                  if (close (fd) == -1)
                {
                  perror ("close");
                  return 1;
                }
            
                  if (buflen < blocksize)
                blocksize = buflen;
            
                  if (*(uint32_t *) p != Magic1)
                {
                  // of0 special case. the file is useless but just for the sake of completness
                  if ((*(uint32_t *) &p[0x10] == Magic1 && blocksize < 512))
                    {
                      decrypt_block (p + 0x10, dst, blocksize, keyA, &counter,
                             &lastkey);
                      sprintf (outname, "%s/of%i", outdir, ofnum);
                      fp = fopen (outname, "w");
                      fwrite (&dst, 1, blocksize - 0x10, fp);
                      fclose (fp);
                      fprintf (stdout, ".of0 file decrypted as %s\n", outname);
            
                    }
                  else
                    {
                      perror ("Invalid file format");
                    }
                }
                  else
                {
                  if (parse_header (p) < 0)
                    fprintf (stdout, "Error parsing header block!\n");
                }
            
                _done:
                  if (munmap (p, buflen) == -1)
                {
                  perror ("munmap");
                  return 1;
                }
            
                }
            
              globfree (&globbuf);
              fprintf (stdout, "\nDone!\n");
            
              return 0;
            }
            
              0

              Спасибо, работает подправленная версия декриптора. Какое устройство исследуете, если не секрет?

                0

                Пока — муки выбора. Наличие расспаковываемой прошивки очень весомый аргумент для меня. А Вы какой апарат порекомендуете?

                  0

                  Ответил в личке.

        0

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

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое