В предыдущей статье я начал разбирать протокол управления блоком питания Fnirsi DPS-150. Там был разобран формат посылки, были выявлены команды и отклики на них, а также сделан черновой анализ внутренностей фирменной программы управления блоком. Детали процесса я обещал показать позже. Вот, эта пора настала. Сегодня мы получим настолько полные таблицы команд и откликов, насколько это требуется для реальной работы.
.
Последовательность разбора
Итак, мы изучаем программу при помощи декомпилятора .Net Reflector. При изучении программы очень хочется идти последовательно. Например, изучить сначала команды, потом – отклики. Или наоборот, сначала отклики, потом команды. Но главное – идти последовательно. Увы! При «раскопках» с отсутствующей отладочной информацией так не получится. Мы всегда будем натыкаться на что-то непонятное. И в таких ситуациях очень важно не пытаться долбиться в глухую стену, иначе можно сначала потерять много времени, а потом – вообще интерес к теме.
Не получается понять команду? Да без проблем! Переходим к следующей. Слишком много непонятных команд? Опять не проблема, посмотрим отклики. Кроме строк в программе, сверяемся также с логами анализатора. Иногда и они помогают. Вот, скажем, мы видим такой фрагмент в логе при открытии блока фирменной программой:
Согласитесь, что выделенное жёлтым – это модель, а выделенное голубым – версия. Давайте подкрасим соответствующие им отклики в шестнадцатеричной части дампа:
Какой идентификатор у жёлтого отклика? 0xde. А у голубого? 0xe0. А давайте поднимем глаза на команды выше. Ба! Знакомые всё лица!
Логично предположить, что это команды, запрашивающие соответствующие отклики. И теперь нам уже придётся не разбираться, что это за две команды и два отклика, а просто удостовериться, что мы трактовали всё верно. И не зря, потому что команды… Напомню, их имена:.
…встречаются только в таком контексте:
Поди, разберись, что они здесь означают, если не было найдено такой замечательной подсказки в логах. А так – мы уже обо всём догадались, и понимаем, что эти две строки как раз запрашивают модель и версию блока питания.
Самые внимательные скажут: «А что это за третья версия? Мы видели в дампе ещё какую-то версию 1.0». А я сам не знаю. Её зачем-то запрашивают и сохраняют, но она нигде не используется. А эти два поля – вот они!
Итак, вывод из раздела: не пытайтесь идти последовательно. В нашем случае, не надейтесь пройти сначала по командам, потом по откликам или наоборот. Будьте готовы при малейшем затыке постараться зайти с другой стороны. В противном случае, есть шанс всё бросить со словами: «Ничего не понятно, надоело оно мне, уже который день гипнотизирую и всё без толку». В моём же случае, всё, что нужно для работы, было вскрыто за один вечер. Разумеется, для статьи пришлось ещё несколько вечеров посидеть, тут уж врать не буду. Но вы же в подобной ситуации будете разбираться не для статьи, а для себя, так что никаких долгих раздумий! Всё странно? Переходите к другой сущности. Ничего не понятно? Переходите к другой подсистеме. Там накопятся какие-то факты. И через некоторое время один из них поможет решить текущую сложность совершенно бесплатно! Проверено! В примере логи раскрыли сущность двух команд, назначение которых из кода вовсе не очевидно.
Элемент везения
Иногда получается найти что-то полезное, используя элемент везения. Вот, скажем, полез я уточнять назначение команды CMD_3. Если честно, не так там про неё много чего-то можно найти, всего три перекрёстных ссылки (что это такое – мы выяснили в прошлой статье):
Первая ссылка – заполнение её тела, вторая ссылка – просто видно, что команда посылается при инициализации подключения среди прочих команд:
Собственно, назначение этой команды потом было найдено при анализе логов этой самой инициализации, но третья ссылка на команду заставила меня на время забыть о ней и переключиться на другие сущности:
Что ещё за UserSelectBox21 и UserSelectBox22? А вот они!
Мы уже знаем, что самый сочный результат даёт функция InitializeComponent(). Идём в неё и смотрим, в каком диалоге мы находимся.
Что в переводе на русский означает «Сохранить текущую группу».
Осматриваемся, где у нас есть группы в программе… А вот они!
Входим, скажем, в группу 5, видим там:
Нажимаем «Сохранить», ловим такие команды в логе:
Наш любимый сайт, который ищется по фразе «float онлайн», говорит, что переданы были именно напряжение и ток (попробуйте декодировать константы 0x40000000 и 0x3f800000 и убедиться в этом сами, если не верите). Странно, что пятёрки не было, я же с пятой группой работал. Как же задаётся её номер? Пробуем группу 4 и вместо команд CD и CE получаем CB и CC. Постепенно, выясняем, что для группы 1 используются команды C5, C6, для группы 2 – C7 и C8, и т.д. Вот так мы поймали команды, которых вообще не было в штатном списке, и непринуждённо изучили их.
А вот третья команда, которую посылала функция… Все уже забыли, наверное, какая функция, лучше повторю её:
Вот третья команда в последовательности — как раз та самая COMMAND_3, код которой в логе 0xff. Что же делает она? И что за простыню она возвращает в ответ? Давайте постепенно переходить к ней.
Все данные из устройства одновременно
Итак, при открытии устройства, посылается команда COMMAND_3, на которую приходит просто огромнейший ответ:
Что это? Из самого дампа ничего не понятно. Но это явно данные. Хорошо, осматриваем класс SerialData и находим там вот такой текст:
В дампе пришло 0x8c байт. Код, разбирающий это дело, просто огромен. В новом редакторе, у меня возникли сложности спрятать его под кат, поэтому приведу только его начало. Кому реально интересно, те полный вариант увидят, когда сами всё откроют в Рефлеторе.
public static void setAllData(byte[] buffer)
{
buffer = getDataArray(buffer);
Data37 = BitConverter.ToSingle(buffer, 0x6f);
Data38 = BitConverter.ToSingle(buffer, 0x73);
Data1 = BitConverter.ToSingle(buffer, 0);
Data2 = BitConverter.ToSingle(buffer, 4);
Data3 = BitConverter.ToSingle(buffer, 8);
Data4 = BitConverter.ToSingle(buffer, 12);
...
Data41 = BitConverter.ToSingle(buffer, 0x7f);
Data42 = BitConverter.ToSingle(buffer, 0x83);
Data43 = BitConverter.ToSingle(buffer, 0x87);
}
В нём анализируются как раз примерно 0x8c байт (если совсем точно, то 0x8b, но у дампа явно сделана поправка на выравнивание).Вот и предположим, что перед нами как раз эти поля.
Проведя минимальное сравнение, выясняем, что всё так и есть. Собственно, программа использует часть полей отсюда как содержимое EEPROM. Мы же выше как раз видели, как сохраняются напряжения и токи для групп. А вот чтобы загрузить – используется эта команда. При работе своей проблемно-ориентированной программы она не особо нужна. Но мы с нею разобрались. А то если назначение команды неизвестно… А вдруг она жизненно необходима? Теперь ясно, что глубже копать смысла нет.
Адресация блоков
Мне кажется, что эту статью будут читать по двум причинам:
1) она недавно опубликована, поэтому просто читатель нашёл её в списке;
2) через месяцы или годы, кто-то вбил в поисковик модель блока питания, чтобы найти описание протокола.
В первом случае читатель не станет вдаваться в самые мелкие подробности. Ему надо показать, как вскрывается протокол, но не с точностью до каждого закоулка кода. Общие черты кто-то просто пролистает как детектив, кто-то может даже закрепит навык, найдя программу на сайте производителя Fnirsi. На днях она переехала у них в раздел Software, раньше почему-то валялась, как Firmware. Но опять же, никто не будет запоминать и проходить все мелочи для блока, которого у него нет и вряд ли окажется. Нужны общие навыки. И нет смысла опускаться до самых мелочей.
А кто пришёл за данными, он уже трясётся от возмущения: «Не буду я это всё пробовать, таблицы команд давай!».
Так что давайте я покажу ещё одну интересную вещь, и закруглюсь с процессом разбора. Я попробую порассуждать про адресацию устройств.
Вообще, в программе можно выбирать адрес блока:
Только блоки все поставляются с номером 1. Поэтому если выбрать любой другой адрес, то после попытки открыть порт он через некоторое время автоматически закроется. Кстати, это всё сделано явно не зря. Если блока на порту нет (скажем, забыли переключить COM1 на актуальный), порт тоже закроется, так что функционал-то нужный.
Но мне почему-то кажется, что в протоколе адреса F0 и F1 соответствуют хосту (нулевое устройство) и блоку с первым адресом. И другие блоки, как я уверен, должны адресоваться, как F2, F3, F4 и т.п. Но мы же видели, что в программе намертво вбито: .
Если я прав, то все эти адреса – фикция, программа работает только с первым. Но в протоколе есть очень интересные рудименты, о которых надо знать. Вот такие данные уходят в порт при открытии:
Устройство же при старте сообщает:
Причём в коде видно, что если в течение определённого времени этот ответ не пришёл, то какая-то кнопка будет отключена. Извините, но это как раз порт закроется!
Поэтому я трактую это так: наша программа говорит в шину: «Всем привет от нулевого».
А устройство говорит: «Всем привет от первого».
Все сообщают свой адрес. А что программа умеет (как я считаю) работать только с первым… а вы найдите смысл делать хотя бы второго! Это же не гроздь устройств на шине RS485! К USB-порту больше одной железки не подключишь! Так что не страшно!
Команды
Всё, переходим к таблицам. Вот таблица команд.
Ком |
Байты |
Назначение |
1 |
0xf1, 0xc1, 0x00, 1 |
Посылается сразу после открытия COM порта |
2 |
0xf1, 0xc1, 0x00, 0 |
Посылается непосредственно перед закрытием порта |
3 |
0xf1, 0xa1, 0xff, 0 |
Запросить полный набор данных оптом |
4 |
0xf1, 0xa1, 0xc0, 0 |
Использование не обнаружено . |
5 |
0xf1, 0xa1, 0xde, 0 |
Запрос модели устройства ??? |
6 |
0xf1, 0xa1, 0xe0, 0 |
Запрос версии прошивки ??? |
7 |
0xf1, 0xa1, 0xe1, 0 |
Есть мнение, что мы сообщаем, что наш адрес равен 0 |
8 |
0xf1, 0xb1, 0xdb, 0 |
Закрыть выход |
9 |
0xf1, 0xb1, 0xdb, 1 |
Открыть выход |
10 |
0xf1, 0xb1, 0xd8, 1 |
Начать измерение потребления (время сбросится в 0) |
11 |
0xf1, 0xb1, 0xd8, 0 |
Прекратить измерение потребления |
12 |
0xf1, 0xa1, 0xd6, X |
Задать яркость экрана (1 байт) |
13 |
0xf1, 0xb0, 0x00, 0 |
Задаёт скорость UART. Индексы скорости: |
14 |
0xf1, 0xa1, 0xc3, 0 |
Использование не обнаружено |
15 |
0xf1, 0xc0, 0x00, 1 |
Перевод устройства в режим обновления «прошивки» |
16 |
0xf1, 0xa1, 0xdf, 0 |
Запрос какой-то версии, которая нигде не отображается |
17 |
0xf1, 0xa1, 0xc1, 0 |
Использование не обнаружено |
18 |
0xf1, 0xa1, 0xc2, 0 |
Использование не обнаружено |
— |
0xf1, 0xb1, 0xc1, val |
Установить заданное напряжение |
— |
0xf1, 0xb1, 0xc2, val |
Установить заданный ток |
— |
0xf1, 0xb1, 0xc5, val |
Сохранить в EEPROM напряжение для группы 1 |
— |
0xf1, 0xb1, 0xc6, val |
Сохранить в EEPROM ток для группы 1 |
— |
0xf1, 0xb1, 0xc7, val |
Сохранить в EEPROM напряжение для группы 2 |
— |
0xf1, 0xb1, 0xc8, val |
Сохранить в EEPROM ток для группы 2 |
— |
0xf1, 0xb1, 0xc9, val |
Сохранить в EEPROM напряжение для группы 3 |
— |
0xf1, 0xb1, 0xca, val |
Сохранить в EEPROM ток для группы 3 |
— |
0xf1, 0xb1, 0xcb, val |
Сохранить в EEPROM напряжение для группы 4 |
— |
0xf1, 0xb1, 0xcc, val |
Сохранить в EEPROM ток для группы 4 |
— |
0xf1, 0xb1, 0xcd, val |
Сохранить в EEPROM напряжение для группы 5 |
— |
0xf1, 0xb1, 0xce, val |
Сохранить в EEPROM ток для группы 5 |
— |
0xf1, 0xb1, 0xcf, val |
Сохранить в EEPROM напряжение для группы 6 |
— |
0xf1, 0xb1, 0xd0, val |
Сохранить в EEPROM ток для группы 6 |
При открытии порта, следует послать:
Перед самым закрытием порта, следует послать:
Инициализационная последовательность выглядит следующим образом:
Отклики
Таблица откликов выглядит следующим образом:
Код |
Имя |
Тип |
Назначение |
0xc0 |
Data. |
float |
Входное напряжение |
0xc3 |
Data4 Data5 Data6 |
float float float |
Активное напряжение Активный ток Активная мощность |
0xc4 |
Data7 |
float |
Температура |
0xd9 |
Data28 |
float |
А*ч (мне кажется, что параметр переведён неверно) |
0xda |
Data29 |
Float |
Вт*ч (мне кажется, что параметр переведён неверно) |
0xdb |
Data30 |
uint8 |
Состояние выхода (открыт или закрыт) |
0xdc |
Data31 |
uint8 |
Светодиод НОРМА (нет аварии) |
0xdd |
Data32 |
uint8 |
Стабилизация напряжения (01) или тока (00) |
0xde |
Data33 |
String |
Модель |
0xdf |
Data34 |
String |
Использование не найдено |
0xe0 |
Data35 |
string |
Версия прошивки |
0xe1 |
Data36 |
uint8 |
Какой-то индекс, пока не ясно, какой. Уж не адрес ли устройства? Пока не пробежит в эфире – не пытаемся слать какие-то команды. Долго не было — отваливаемся |
0xe2 |
Data37 |
float |
Верхний предел напряжения, которое можно запросить (входное минус падение на стабилизаторе) |
0xe3 |
Data38 |
float |
Верхний предел тока, который можно установить |
0xff |
— |
— |
Все данные оптом |
Заключение
Мы познакомились с расширенными методами разбора протокола на примере блока питания Fnirsi DPS-150. Приведённых сведений достаточно для того, чтобы разработать собственную программу для управления блоком.
Программа, разработанная автором, весьма специфична, поэтому в чистом виде приводиться не будет, но в планах – сделать статью, разъясняющую одну интересную технологию, на примере этого протокола. Там будет реальный код. Разумеется, если тема окажется интересной для читателя.