Детали протокола управления блоком питания Fnirsi DPS-150

В предыдущей статье я начал разбирать протокол управления блоком питания Fnirsi DPS-150. Там был разобран формат посылки, были выявлены команды и отклики на них, а также сделан черновой анализ внутренностей фирменной программы управления блоком. Детали процесса я обещал показать позже. Вот, эта пора настала. Сегодня мы получим настолько полные таблицы команд и откликов, насколько это требуется для реальной работы.

fbeacd40764703a285ba685fcc1837c9.png

Последовательность разбора

Итак, мы изучаем программу при помощи декомпилятора .Net Reflector. При изучении программы очень хочется идти последовательно. Например, изучить сначала команды, потом — отклики. Или наоборот, сначала отклики, потом команды. Но главное — идти последовательно. Увы! При «раскопках» с отсутствующей отладочной информацией так не получится. Мы всегда будем натыкаться на что-то непонятное. И в таких ситуациях очень важно не пытаться долбиться в глухую стену, иначе можно сначала потерять много времени, а потом — вообще интерес к теме.

Не получается понять команду? Да без проблем! Переходим к следующей. Слишком много непонятных команд? Опять не проблема, посмотрим отклики. Кроме строк в программе, сверяемся также с логами анализатора. Иногда и они помогают.  Вот, скажем, мы видим такой фрагмент в логе при открытии блока фирменной программой:

697cb42e5aa977155889dfdf82a985e1.PNG

Согласитесь, что выделенное жёлтым — это модель, а выделенное голубым — версия. Давайте подкрасим соответствующие им отклики в шестнадцатеричной части дампа:

b36b32be7335682522e1cc726e54404b.PNG

Какой идентификатор у жёлтого отклика? 0xde. А у голубого? 0xe0. А давайте поднимем глаза на команды выше. Ба! Знакомые всё лица!

849680bf59fa587007127a212db7403c.PNG

Логично предположить, что это команды, запрашивающие соответствующие отклики. И теперь нам уже придётся не разбираться, что это за две команды и два отклика, а просто удостовериться, что мы трактовали всё верно. И не зря, потому что команды… Напомню, их имена:

3bf74ad0baa7d19e7735abc4c092a612.png

…встречаются только в таком контексте:

26052250d9ccf72feb23096c78e591c9.PNG

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

Самые внимательные скажут: «А что это за третья версия? Мы видели в дампе ещё какую-то версию 1.0». А я сам не знаю. Её зачем-то запрашивают и сохраняют, но она нигде не используется.  А эти два поля — вот они!

e263c645cf693044dc49a2d493eddb04.PNG

Итак, вывод из раздела: не пытайтесь идти последовательно. В нашем случае, не надейтесь пройти сначала по командам, потом по откликам или наоборот. Будьте готовы при малейшем затыке постараться зайти с другой стороны. В противном случае, есть шанс всё бросить со словами: «Ничего не понятно, надоело оно мне, уже который день гипнотизирую и всё без толку». В моём же случае, всё, что нужно для работы, было вскрыто за один вечер. Разумеется, для статьи пришлось ещё несколько вечеров посидеть, тут уж врать не буду. Но вы же в подобной ситуации будете разбираться не для статьи, а для себя, так что никаких долгих раздумий! Всё странно? Переходите к другой сущности. Ничего не понятно? Переходите к другой подсистеме. Там накопятся какие-то факты. И через некоторое время один из них поможет решить текущую сложность совершенно бесплатно! Проверено! В примере логи раскрыли сущность двух команд, назначение которых из кода вовсе не очевидно.

Элемент везения

Иногда получается найти что-то полезное, используя элемент везения. Вот, скажем, полез я уточнять назначение команды CMD_3. Если честно, не так там про неё много чего-то можно найти, всего три перекрёстных ссылки (что это такое — мы выяснили в прошлой статье):

99bf764cc9cfd315b6943d925fa93c91.png

Первая ссылка — заполнение её тела, вторая ссылка — просто видно, что команда посылается при инициализации подключения среди прочих команд:

86e745dbd677e417740fe372dd7780cc.PNG

Собственно, назначение этой команды потом было найдено при анализе логов этой самой инициализации, но третья ссылка на команду заставила меня на время забыть о ней и переключиться на другие сущности:

8abef955087de809b95e9de826b0e1c3.png

Что ещё за UserSelectBox21 и UserSelectBox22? А вот они!

6d2b58ee3ed2f287c0aea95d6839f3cf.png

Мы уже знаем, что самый сочный результат даёт функция InitializeComponent (). Идём в неё и смотрим, в каком диалоге мы находимся.

6fdc03864c6167fe208db0c10f227b6d.png

Что в переводе на русский означает «Сохранить текущую группу».

Осматриваемся, где у нас есть группы в программе… А вот они!

4e3ad9eefe00d2fd24bd7860cd01922a.png

Входим, скажем, в группу 5, видим там:

03e12040d478b8d4e5952535ce44e1c1.png

Нажимаем «Сохранить», ловим такие команды в логе:

94383d15e8e938788c2ad4f08be29915.PNG

Наш любимый сайт, который ищется по фразе «float онлайн», говорит, что переданы были именно напряжение и ток (попробуйте декодировать константы 0×40000000 и 0×3f800000 и убедиться в этом сами, если не верите). Странно, что пятёрки не было, я же с пятой группой работал. Как же задаётся её номер? Пробуем группу 4 и вместо команд CD и CE получаем CB и CC. Постепенно, выясняем, что для группы 1 используются команды C5, C6, для группы 2 — C7 и C8, и т.д. Вот так мы поймали команды, которых вообще не было в штатном списке, и непринуждённо изучили их.

А вот третья команда, которую посылала функция… Все уже забыли, наверное, какая функция, лучше повторю её:

5a7397b116681a491f49172bb81d06c3.png

Вот третья команда в последовательности — как раз та самая COMMAND_3, код которой в логе 0xff. Что же делает она? И что за простыню она возвращает в ответ? Давайте постепенно переходить к ней.

Все данные из устройства одновременно

Итак, при открытии устройства, посылается команда COMMAND_3, на которую приходит просто огромнейший ответ:

2c756a459e4fb9e800b404452244d0c3.PNG

Что это? Из самого дампа ничего не понятно. Но это явно данные. Хорошо, осматриваем класс SerialData и находим там вот такой текст:

110734627f6d67f51319a513e455352e.png

В дампе пришло 0×8c байт. Код, разбирающий это дело, просто огромен. В новом редакторе, у меня возникли сложности спрятать его под кат, поэтому приведу только его начало. Кому реально интересно, те полный вариант увидят, когда сами всё откроют в Рефлеторе. 

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);
}

В нём анализируются как раз примерно 0×8c байт (если совсем точно, то 0×8b, но у дампа явно сделана поправка на выравнивание).Вот и предположим, что перед нами как раз эти поля.

Проведя минимальное сравнение, выясняем, что всё так и есть. Собственно, программа использует часть полей отсюда как содержимое EEPROM. Мы же выше как раз видели, как сохраняются напряжения и токи для групп. А вот чтобы загрузить — используется эта команда. При работе своей проблемно-ориентированной программы она не особо нужна. Но мы с нею разобрались. А то если назначение команды неизвестно… А вдруг она жизненно необходима? Теперь ясно, что глубже копать смысла нет.

Адресация блоков

Мне кажется, что эту статью будут читать по двум причинам:

1)      она недавно опубликована, поэтому просто читатель нашёл её в списке;
2)      через месяцы или годы, кто-то вбил в поисковик модель блока питания, чтобы найти описание протокола.

В первом случае читатель не станет вдаваться в самые мелкие подробности. Ему надо показать, как вскрывается протокол, но не с точностью до каждого закоулка кода. Общие черты кто-то просто пролистает как детектив, кто-то может даже закрепит навык, найдя программу на сайте производителя Fnirsi. На днях она переехала у них в раздел Software, раньше почему-то валялась, как Firmware.  Но опять же, никто не будет запоминать и проходить все мелочи для блока, которого у него нет и вряд ли окажется. Нужны общие навыки. И нет смысла опускаться до самых мелочей.

А кто пришёл за данными, он уже трясётся от возмущения: «Не буду я это всё пробовать, таблицы команд давай!».

Так что давайте я покажу ещё одну интересную вещь, и закруглюсь с процессом разбора. Я попробую порассуждать про адресацию устройств.

Вообще, в программе можно выбирать адрес блока:

66b81205dca1dfaa7b47e83ea67219e6.PNG

Только блоки все поставляются с номером 1. Поэтому если выбрать любой другой адрес, то после попытки открыть порт он через некоторое время автоматически закроется. Кстати, это всё сделано явно не зря. Если блока на порту нет (скажем, забыли переключить COM1 на актуальный), порт тоже закроется, так что функционал-то нужный…

Но мне почему-то кажется, что в протоколе адреса F0 и F1 соответствуют хосту (нулевое устройство) и блоку с первым адресом. И другие блоки, как я уверен, должны адресоваться, как F2, F3, F4 и т.п. Но мы же видели, что в программе намертво вбито:

978af78ff1a3569b17f4b2cef490acbf.PNG

Если я прав, то все эти адреса — фикция, программа работает только с первым. Но в протоколе есть очень интересные рудименты, о которых надо знать. Вот такие данные уходят в порт при открытии:

09b2a8f8f1937a366844bb180bc97a6e.PNG

Устройство же при старте сообщает:

3018532a0318b65436af17f57f979d7d.PNG

Причём в коде видно, что если в течение определённого времени этот ответ не пришёл, то какая-то кнопка будет отключена. Извините, но это как раз порт закроется!

Поэтому я трактую это так: наша программа говорит в шину: «Всем привет от нулевого».

46c0aa6f49ec5e31ee67b60e67b0547f.PNG

А устройство говорит: «Всем привет от первого».

ec11ea6e5a2f67592c17234b548e9586.PNG

Все сообщают свой адрес. А что программа умеет (как я считаю) работать только с первым…, а вы найдите смысл делать хотя бы второго! Это же не гроздь устройств на шине RS485! К USB-порту больше одной железки не подключишь!   Так что не страшно!

Команды

Всё, переходим к таблицам. Вот таблица команд.

Ком

Байты

Назначение

1

0xf1, 0xc1, 0×00, 1

Посылается сразу после открытия COM порта

2

0xf1, 0xc1, 0×00, 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, 0×00, 0

Задаёт скорость UART. Индексы скорости:
1:9600, 2:19200,3:38400,4: 57600, 5:115200

14

0xf1, 0xa1, 0xc3, 0

Использование не обнаружено

15

0xf1, 0xc0, 0×00, 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

При открытии порта, следует послать:

0d6a86f88348c04b0450d7cedda2c481.png

Перед самым закрытием порта, следует послать:

4ea687cb2e52fa03e7da5f466a298829.png

Инициализационная последовательность выглядит следующим образом:

925ed44f230b10568cff4687b63a2d59.png

Отклики

Таблица откликов выглядит следующим образом:

Код

Имя

Тип

Назначение

0xc0

Data1

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. Приведённых сведений достаточно для того, чтобы разработать собственную программу для управления блоком.

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

© Habrahabr.ru