Сегодня я начну рассказывать, как изучал протокол программного управления блоком питания Fnirsi DPS 150. До подробного описания всех команд и откликов мы в этот раз, правда, не дойдём, но зато рассмотрим шаги, которые обязательно в итоге приведут нас к успеху. То есть, эта часть – не руководство, как программировать блок, а описание, как вскрывался протокол. Кому это интересно – начинаем.
Введение
В прошлой статье я продвинул идею, что для предотвращения «восстания машин», знаниями и найденными методиками надо обязательно делиться через Хабр. Но при этом сам же давно уже почти ничего не описываю, так как считаю информацию, которая сейчас пробегает через меня, не самой существенной (а существенную злые Заказчики закрывают через соглашение о неразглашении, или по-русски – NDA).
Но надо же исполнять свои слова, так что давайте, пока ещё сильны воспоминания, я упорядочу свои черновики, описав процесс постижения весьма нестандартного протокола управления блоком питания Fnirsi DPS 150. Может, кому-то пригодится если не сам протокол, то именно методика, при помощи которой он вскроет что-то подобное. Ну, а может, через годы, кто-то выйдет на статью, когда будет искать сам протокол.
Почему выбрана именно эта модель блока? Мне надо было протестировать одну поделку, как она себя поведёт при разных профилях питающего напряжения. Но при этом, вести проверку надо было, среди прочего, в походных условиях, поэтому источники, работающие от сети 220 вольт, не подходили. Рассматриваемая же модель получает на вход уже постоянку либо с классического круглого разъёма 5,5x2,5 мм, либо с USB Type C по протоколу Power Delivery.
Если взять Power Bank, поддерживающий Power Delivery, можно вполне себе решить мою задачу. Короче, очень удобный источник для стоявшей задачи. Поэтому на нём и остановился. Для работы дома я эти паровозики (220->20->нужное напряжение) не люблю, для дома у меня давно куплен блок, который в розетку втыкается, но как видим, случаи-то разные бывают!
Кстати, раз уж рассматриваем фото, то отмечу забавный факт. Питание идёт по прогрессивному протоколу Power Delivery, а вот для подключения управляющей ЭВМ используется разъём MicroUSB, который нынче многие считают устаревшим. Но это так, лирическое отступление. Зато точно не перепутаем, куда подавать питание, а куда управление.
Эксперименты хотелось провести быстро, так что ждать доставку с Ali Express было некогда. На Озоне же нашёлся вариант с доставкой «прямо завтра». Ну и ладушки. Вот так именно этот блок и оказался у меня. По размеру он примерно с мой фотоаппарат, его внешний вид был представлен на вступительной картинке к статье. У него ещё эффектно откидывается экран, но фотки с откинутым экраном есть в обзорах, а у этой статьи чуть иная задача. Нам надо не внешний вид и штатный функционал изучать, а учиться управлять им из своих программ. Вот и приступаем
Источники знаний о протоколе
Хотелось бы, чтобы протокол был описан в документе от производителя. Увы, такого документа найдено не было. Может, он и существует, но не обнаружен. А жалко.
Не было обнаружено и любительских проектов, управляющих данным источником. Возможно, он слишком новый (надеюсь, именно поэтому, а не потому, что имеет какие-нибудь недостатки, из-за которых на него никто не смотрит).
Зато на сайте производителя, есть программа под Windows, которая этим источником управляет. Мало того, в “продвинутом” режиме у неё есть даже какая-то возможность гонять профили:
Но есть один нюанс: многие кнопки, при нажатии, приводят к такому результату:
Исключение вылетает даже при обычном закрытии окна программы. Так что я не буду рассуждать о том, что мне надо было задавать профиль не по таймеру, как в этой программе, а с привязкой к командам в моё устройство. То, что фирменная программа вылетает – это более весомый аргумент, почему ею нельзя пользоваться, а надо писать свою. Хотя мне всё равно она бы и не подошла. Мне действительно надо было привязывать установку напряжений к вызову функционала моего устройства. Ну, и логи анализа потребляемого тока было проще привязывать к вызванному функционалу, чем потом совмещать по времени.
В общем, фирменная программа, хоть и глючна, а всё равно это огромное подспорье при изучении протокола, по которому она общается с устройством, поэтому черпать вдохновение мы будем из работы с ней, а, как потом выяснилось, и из неё самой.
Что за протокол
Разберёмся, что же за протокол у блока. Может, он какой-то стандартный. Очень многие производители любят делать что-то, похожее на классический GPIB… Вот в том блоке, который у меня от сети 220 вольт работает, реализован именно тот протокол, хоть я его ещё и не изучал. А какой протокол у DPS 150?
При подключении кабеля MicroUSB в системе появляется COM-порт. Ну, хорошо. Запускаем свой любимый сниффер COM-порта. Лично я предпочитаю Bus Hound. Открываем, активируем анализ порта. Дальше запускаем фирменную программу, выбираем в ней порт и нажимаем кнопку «В Сети». Именно так китайские друзья перевели английское слово Online.
Сниффер начинает ловить байтики. Пока я опущу входящие данные, которые бегут непрерывно, раз в 500 мс. Они какие-то пока что непонятные. Нам же надо нажать что-то, заведомо известное и простое, и посмотреть, что улетит в устройство. По умолчанию, в программе почему-то было установлено напряжение 3.5В. Вот я нажимаю на кнопочку «Вниз», чтобы получить 3.49.
Сразу на выход подаётся строка:
Пока что не очень понятно, что это значит. Как минимум, троек в посылке нет (целая часть равна трём вольтам). Но давайте проверим, не передаётся ли напряжение в стандартном вещественном формате. Идём на Гугля и задаём поисковую строку float онлайн (второе слово именно по-русски). Я эту поисковую строку придумал, когда делал лекцию для студентов про упаковку вещественных чисел. Сегодня нам пригодится не сам процесс упаковки, а готовый результат на выходе. Попадаем на русскоязычный сайт http://floatingpoint.ru/online/float2dec.php . Там переходим на страницу Перевод из десятичной системы во float ieee-754 (черновая версия). Вбиваем 3.49, переводим, получаем:
А что у нас было в команде?
С поправкой на переставленные байты – как раз оно, как раз 0x405f5c29.
Аналогично задаётся ток (я проверил, это тоже корректное значение типа float в Little Endian)
Имея эти результаты, я поискал в сети разные протоколы… Иногда этого оказывается достаточно, и можно обнаружить документ, или исходники на ГитХабе. Но в этот раз, я ничего не нашёл. Нет ни описаний, ни каких-либо проектов под этот блок питания. Производители явно не горят желанием этот протокол публиковать, а любители – ещё не освоили его. Ну хорошо, давайте анализировать протокол чуть более детально.
Выделяем сущности протокола
Давайте теперь посмотрим на протокол более широко. Осмотрим также строки, которые идут из устройства в программу. Причём сначала во мне сработала инертность мышления. Я почему-то решил, что раз у нас USB, то и данные худо-бедно разделены на пакеты. Поэтому рассматривал их так, как они выдавались в логе анализатора. Вот последовательность, где каждая транзакция начинается со слова IN или OUT:
Видны структуры, у которых имеется некий трёхбайтный префикс (красное), затем – длина данных (жёлтое выделение), сами данные (серое выделение) и ещё байт (голубое выделение).
Уже позже, я сообразил, что мы же имеем дело с UART-ом! А там данные идут не транзакциями, а потоком! Все эти разделения на псевдопакеты происходят, когда программа обратилась к драйверу за данными. Не надо задумываться, почему в один пакет в логе объединено несколько сущностей! Нет там никаких правил, кроме времени обращения программы к порту. В очередную транзакцию попадёт то, что успело за прошедший временной промежуток накопиться в буфере. Так что сущности выделены корректно, но сам поток данных выглядит чуть проще. Запишем его в реальном виде:
При таком раскладе, голубая сущность наиболее вероятно, относится к контрольной сумме, хоть правило её вычисления пока и не ясно. Но какие правила заполнения у красной сущности? Слишком много неизвестных! А программа от производителя такая заманчивая! Она состоит из одного исполняемого файла! Её так и хочется посмотреть изнутри! Что ж! Займёмся этим!
Команды
Как я уже отмечал тут, с некоторых пор, дизассемблер/декомпилятор Гидра мне нравится больше, чем дизассемблер IDA. Но вот беда, при попытке открыть фирменную программу Гидрой, выясняем, что она .NET-овская. А Гидра .NET не любит. IDA же, не знаю, как сейчас, а в былые времена вскрывала .NET только на уровне ассемблера, что не совсем удобно.
В общем, будем разглядывать прелести программы при помощи инструмента по имени .NET Reflector. Кто не знаком с ним – не бойтесь. Я сам им пользуюсь в третий раз в жизни. Причём из первых двух случаев знаю, что это простейший и интуитивно понятный инструмент. Он относится к разряду декомпиляторов, так что мы будем получать не ассемблерный код, а исходный код на одном из выбранных языков.
Правда, сегодня я выбирать язык не буду. Что автоматически будет выбрано, тем и воспользуюсь. Какая разница, на чём логику восстанавливать? Скриншот я добавлял уже когда причёсывал статью. Оказывается, по умолчанию, был выбран C#. Вполне себе приличный язык!
Загружаем EXE-шник внутрь Рефлектора, видим в дереве объектов что-то сильно нерусское…
Страшно? Мне тоже! Но давайте посмотрим внутрь первой из этих сущностей:
Ага, уже менее страшно. Уже понятней. Даже есть классы для работы с командами (Command) и последовательным портом (Serial). Вот давайте с команд и начнём. Наверняка там найдётся что-нибудь интересное. Поля класса содержат некий список команд.
Уже видно, сколько команд используется программой (позже выяснится, что чуточку больше, но не сильно). Жаль, что не ясно, какая из них для чего. Имена у них совершенно не говорящие. Ну, да ладно. Нам бы сначала с форматом разобраться. Посмотрим на конструктор этого класса. Слева выбираем конструктор в дереве, справа видим его тело:
Уж не те ли это три байта, что идут в начале команды расположены в началах конструкторов объектов CMD_1 и т.д.? Просто зачем-то хитрые авторы размазали команду на три байта…
Мы уже видели в логах, как задавались напряжение и ток:
Очень похоже, но последовательностей 0xf1, 0xb1, 0xc1 и 0xf1, 0xb1, 0xc2 не встречается. Неужели ошибка? К счастью, программа не такая большая, гуляя по её визуальной части, находим:
То есть, именные команды именными командами, но бывают ещё и особые команды, конструируемые прямо в визуальных элементах. Будем иметь в виду, благо их мало. А пока – возвращаемся к командам в целом. Мы выяснили, что самым важным тут является класс CmdModule (Command наследуется от него, а иногда его объекты конструируются налету). Идём в него. Вот такой у этого класса конструктор:
Всё логично. Три странных байта, затем – данные. А где к этому делу добавляются длина и контрольная сумма? Осматриваясь внутри этого же класса, видим:
Прекрасно! Видно, что байт C4 – это как раз длина (считаем с единицы: байты C1, C2, C3 – константы, C4 – длина, именно это мы видели в логах). Теперь мы знаем, что первые три байта – это не совсем разные поля. С точки зрения данной программы, это именно одна 24-битная константа. Почему она такая? Пока не знаю, хоть ниже и предположу.
А вот контрольная сумма (байт C6) вычисляется весьма забавно. Байты C1 и C2 в ней не участвуют! Является ли это хитростью, чтобы не все поняли протокол, или несёт какую-то суть – на момент осмотра не знал. Знаю, что в нашем коде это надо учитывать. Контрольная сумма считается, начиная с байта C3. Опять же, ниже я выскажу предположение, почему это сделано именно так. Но чтобы то предположение сформулировать до конца, надо рассмотреть ответы от устройства, так что сначала займёмся этим.
Ответы от устройства
Что можно узнать начерно про ответы от устройства? Почему там есть ответы с длиной данных 0x04 байта, а есть – целых 0x0c байт? Давайте бегло осмотрим код.
Пробегая по дереву классов, находим наиболее вероятное хранилище:
Ну, явно же поля, пришедшие из COM-порта, тут могут храниться! Data1, Data2 и т.д. Как бы это подтвердить? Перебираем все функции класса и находим вот такую красавицу:
Давайте предположим, что на вход ей приходит буфер. В отличие от команды, где рассматривались три байта, которые именовались c1, c2, c3, тут буфер – это массив. Он индексируется с нуля. Тогда строка:
как раз изымает третий элемент (считаем от нуля) и использует его как длину. То есть, выделяет полезные данные. Отлично. А второй байт (считаем от нуля) содержит идентификатор (аналог команды, но мы же ответы анализируем, так что назовём его идентификатором). Итак, вот нам пришло:
0xf0 и 0xa1 будут проигнорированы рассматриваемым кодом, 0xc3 будет идентификатором, 0x0c – длиной, в buffer2 попадёт тело данных. А что в коде соответствует идентификатору 0xc3?
Single – это псевдоним float, вещественное число одинарной точности. В составе пакета приходит три вещественных числа, которые лягут в переменные Data4, Data5 и Data6. Хорошо. Пока не разбираясь в программе, посмотрим на эти поля:
Извините, но судя по форматированию, это нам явно передаются текущие напряжение, ток и мощность (измеряемые в Вольтах – V, Амперах – A и Ваттах – W). Да, внешнее питание отключено, сейчас в логе всё по нулям.
Проверяем себя
Чтобы убедиться, что перед нами не случайное совпадение, давайте для надёжности попробуем понять, что нам пытаются сообщить в такой посылке:
Смотрим ранее найденный код:
Вещественное число, по смещению 0. На моём любимом (том самом, что ищется по фразе float онлайн) сайте выясняем, что там закодировано +18.7819976806640625. По порядку и примерному значению, я уже понимаю, что перед нами входное напряжение, но давайте отработаем методику, ведь потом будут данные, назначение которых предсказать невозможно. Итак, эти данные попадут в поле Data1. Как описаны функции его записи и чтения?
Точно! Вольты. Но как бы понять, что это именно те самые входные вольты, а не какие-то ещё? Расскажу, как это делается в данном случае. Есть там некоторая вероятность, что методика не сработает. Но ведь у меня же сработала!
Идём в класс Data1 (просто щёлкнув по нему в исходнике). Попадаем сюда:
Видим там в функции set такой паровозик:
То есть, эта переменная попадает в объект userButton113. Щёлкаем мышкой по этому слову, попадаем сюда:
Работа с перекрёстными ссылками
Ничего, за что можно было бы зацепиться! Но сейчас я расскажу про очень мощный метод анализа: ставим курсор на элемент дерева, нажимаем правую кнопку «мыши» и выбираем пункт меню Analyze:
Справа снизу появляется интересное дерево, раскроем его:
Раскроем Used By:
Перед нами список всех мест, откуда кто-то зачем-то обращается к этому объекту. Для UserButton113, таких обращений два. Первое из них – оно не интересное. Мы как раз оттуда пришли. А вот второе… На нём нажимаем правую кнопку «Мыши» и выбираем Go To Member:
Получаем огромнейший текст инициализатора, который заполняет окно элементами. Давайте понадеемся (благо это сработает, я проверил), что перед отображением элемента будет выводиться поясняющий текст. Привычно нажимаем в окне с тем огромным кодом комбинацию Ctrl+F и ищем всё, что связано с userButton113. Сначала будет находиться скукотень, типа такой:
Но через некоторое время найдётся целый блок, настраивающий эту кнопку:
Как я сказал, нас интересует не сама кнопка, а элемент, подписывающий её. Потому что на экране, если не был изменён язык, всё выглядит так:
Вот тот текст и находится выше, элемент label9. Берём фразу输入电压 в буфер обмена и скармливаем её переводчику. Получаем «входное напряжение», что и требовалось доказать.
Поздравляю! Мы только что изучили методику работы с перекрёстными ссылками. Она является основополагающей при анализе кода. В Рефлекторе для её вызова надо вызвать меню Analyze. А затем найти в дереве ветвь Used By. Если перекрёстных ссылок много, ветвь будет большая.
Когда элемент отработан, его можно удалить, выбрав Remove в контекстном меню:
Что дальше
Учитывая, что данных в блоке case не так и много, мы можем относительно быстро вскрыть весь протокол. Желающие могут потренироваться по образу и подобию самостоятельно. Но размер статьи такой, что наверняка читатели уже устали. А уж как устал автор – кто бы знал! Поэтому упорядочивание полученных сведений произведём в следующей части.
Почему контрольная сумма такая удивительная
Напомню, что первые два байта команды не учитываются в контрольной сумме. А давайте предположим, что в составе протокола они адресные. Бывает, что передаётся адрес приёмника и адрес источника. Это характерно для протоколов, идущих по интерфейсу RS485. Там обычно на одной шине висит целая гроздь устройств, и они всегда сообщают, кому передают данные. Но в нашем случае, похоже, передаётся адрес и субадрес. Этот вывод я сделал потому, что данные от программы к устройству идут так:
А от устройства к программе – так:
Если бы были адрес приёмника и источника, то были бы «Юстас Алексу» и «Алекс Юстасу». Первые два байта бы просто менялись местами. А в реальности, второй байт содержит что-то другое.
Напомню, в коде начало списка команд, найденных в конструкторе класса Command выглядит так:
В начале всегда 0xf1. Это может быть адрес приёмника. А у второй половины старший ниббл имеет значения A, B или C, младший ниббл – от нуля до двух.
Первая пара байтов ответа не анализируется, но мы можем проверить их значения в логе. Не буду загромождать текст, первый байт всегда 0xf0 (адрес приёмника?), Второй – тоже AX, BX или CX. Субадрес?
Ну, а F0 – нулевое устройство. ПК. F1 – первое устройство. Вполне логично. Точное назначение субадреса пока не выявлено, но по крайней мере, почему первая пара байтов в посылке не учитывается в контрольной сумме, становится понятно. Они не относятся к данным. Поэтому и не участвуют в расчёте КС.
На шине USB все эти адреса скорее задают совместимость с протоколами, передаваемыми через настоящий UART (а точнее, через RS485). Контрольная сумма, кстати – тоже. На самой шине USB данные защищены при помощи CRC. Но мы не придумываем протокол, мы просто изучаем, как им пользоваться. И теперь мы примерно понимаем, почему всё сделано так, а не иначе. Детальнее нам всё равно не требуется.
Заключение
В статье рассмотрена методика разбора протокола управления программируемым блоком питания FNIRSI DPS150. От простого анализа трафика произведён переход к анализу кода сервисной программы. Выявлен формат команд и возвращаемых данных. Рассмотрена общая методика получения назначения всех возвращаемых данных.
Детализация протокола будет произведена в следующей части статьи.
Комментарии (14)
x89377
29.10.2024 11:53А исходники после рефлектора на github не выложены ?
EasyLy Автор
29.10.2024 11:53Если исходники, которые были написаны, то они весьма специфичны, от них толку никому не будет. Во второй части статьи будут таблицы команд (черновики уже имеются, надо дооформить статью), а дальше - можно будет сделать демонстрационный проект небольшой. Он в планах. .
GiaNT_05
29.10.2024 11:53Спасибо за статью. Советую вам приглянуться к более развитому декомпилятору под .NET - DnSpyEx. Это форк и продолжение декомпилятора DnSpy.
Тоже весьма неплохое решение в таких задачах.
NutsUnderline
29.10.2024 11:53Писал студент-недоучка программу эту....
EasyLy Автор
29.10.2024 11:53Может и так, но может просто стиль у них в Китае такой.
Меня как-то давненько знакомый попросил поменять один предел настроек в паяльнике TS-100. Я взял за основу не сторонние решения с Гитхаба, а исходники от производителя паяльника. Чтобы всё было так, как привычно тому знакомому.
И вот смотрю я исходники. Не дизассемблятину, а именно исходники!!! И не вижу, как задаются пределы. Час бегал курсором... В конце концов, оказалось, что они проверяют не числовые значения, а строки. Вот так: "Первый символ не может быть меньше нуля и больше стольки-то, второй - от нуля до девяти", и так - по всем возможным разрядам. А может ещё там варианты были, если на 00 кончается и не на 00, то первый символ разный. Детали уже стёрлись в памяти, но помню, что изврат был дикий! У нас так не пишут. У нас преобразуют к числу и сравнивают одним махом.
В общем, у них свои традиции программирования. Но это не исключает, что и студент мог писать. Одно другому не мешает.
NutsUnderline
29.10.2024 11:53это они позаимстовали код оттуда где цифровая клавиатура для набора числа - для такого более логично выглядит
N-Cube
Если у вас есть гальваническая развязка для осциллографа и компьютера, то можно и так. А если нет, то источник питания, осциллограф и ноутбук лучше от батарей запитать. Можете найти на ютубе множество случаев, что бывает, если это не учесть.
EasyLy Автор
В ряде случаев - да. Но чаще всего меня спасает обратное. Когда всё соединено хорошей землёй.
Но таки да. Периодически берётся USB осциллограф, а сам ноутбук отключается от внешнего источника (а значит, внешней земли). И они в паре живут с отдельной от всего остального землёй.
Но такое требуется раз в несколько лет при работах с цифровыми устройствами. А я всё больше с ними работаю. С ними хорошая общая земля наоборот полезней.
N-Cube
Но зачем вам общая земля "в розетке"? Никаких плюсов нет, а проблем много. Общая земля нужна для вашего USB осциллографа и устройства. Притом, можно сделать развязку (см. чипы и девайсы для изоляции усб) между ноутбуком и усб осциллографом, тогда ноутбук можно и к сети подключить.
EasyLy Автор
Можно... И я ни на чём не настаиваю. Но уже лет 20 у меня всё работает.
Каждый пользуется своим вариантом под свои задачи. Мне удобнее, чтобы всё было соединено землёй хотя бы в удлинителях. Потому что я работаю с таким железом, которому это нравится. Наоборот, были случаи, когда я приходил к тем, у кого ничего не работает, и лечение заключалось в улучшении земли.
И это же замечательно, что есть оборудование под любые потребности! И не надо либо одним всё время всё жечь, либо другим тратиться на то, на что им тратиться не нужно! Все довольны!
У меня были случаи, когда надо было землю осциллографа отрывать. Ну я тогда просто на батарейное питание переходил. Перешёл, полчаса поработал, вернулся к тому, что всегда. Пару лет не нужно... И ради получаса в пару (а то и пяток) лет городить огород? Такие уж у меня задачи... И у многих цифровиков - тоже.
N-Cube
Интересный у вас выбор источника питания - мы его брали как раз за возможность работы от батареи :) Кстати, это становится трендом и в более профессиональном оборудовании - например, новые осциллографы (ригол) тоже от усб-си питаются. Если раньше люди городили два или четыре автомобильных аккумулятора (от 48 вольт постоянного напряжения вместо 220 переменного прежние риголы тоже работают), то теперь достаточно подключить к пауэрбанку :)
EasyLy Автор
Да, стандарт PowerDelivery совершил переворот. Теперь, если надо БЫСТРО, то можно просто купить то, что питается от него, взять соответствующий PowerBank, и рвануть на природу, проводить опыты. Человеко-часы на подготовку чего-то своего, они же тоже денег стоят.
А оборудование теперь будет готово к новым свершениям. Но при всём при этом, если надо что-то классическое сделать - буду использовать старый блок питания, который напрямую в розетку втыкается, а не через ещё один блок... Мне так проще. О чём я и отметил. Ну не люблю я паровозики.
N-Cube
Фото рабочего места не хватает! Если к статье поставите фотографию с оборудованием, то и заметить статью легче будет и лишние вопросы отпадут. При наличии места можно и блок розеток с гальванической развязкой оборудовать, почему бы и нет, ну а если это не основное занятие и место ограничено, то тут без вариантов - компактные устройства с возможностью питания от батареи выигрывают по всем фронтам. В вашей терминологии, компактный «паровозик» годится для дома, а вот столь любимые вами большие паровозы требуют целого паровозного депо :)