Ода структурам на примере работы с блоком питания Fnirsi DPS 150

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
В предыдущих двух статьях цикла, мы сначала научились разбирать протокол для управления блоком питания FNIRSI DPS150, а затем – изучили все основные команды для работы с ним. Теперь с одной стороны, было бы полезно показать готовую программу для работы с этим блоком питания, но с другой… А уровень ли это Хабра? Ну программа, ну для управления железкой… Да мы, системные программисты, по три библиотеки в день пишем на основе готовых команд… Нет! Для уровня Хабра в статье должна быть какая-то изюминка!

Поэтому я решил при разработке функционала, подробно рассказать про важность перехода от структур к «сырым» данным и обратно, от «сырых» данных к структурам. А чтобы было интереснее, сделаю это не только на языках C/C++, но ещё и на Питоне.

Кому интересно, приступаем!



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

1 Немного теории о структурах


Основное назначение структур в языках программирования, как ни банально это звучит, помогать структурировать данные. В стародавние времена, когда в обычных школах всем преподавали Бейсик, на подготовительных курсах в Политехе, где имелись солидные ЭВМ ДВК-3, уже обучали Паскалю. Поэтому, что такое «Запись» (Record), нам, старшеклассникам, разъяснили очень быстро. Сначала мы старательно тренировались объединять в них ФИО, телефон и дату рождения каких-то виртуальных людей, но потом выяснилось, что без записей сложно построить такие элементы данных, как список и дерево. В общем, без них никуда! Очень полезные штуки!

Как-то всю учёбу в ВУЗе мы пролетели на Паскале, поэтому о том, что в языке Си (который разрабатывался как высокоуровневая замена ассемблеру PDP-11), структуры, совместно с другими сущностями, решают и задачи стыка с аппаратурой, я узнал уже в этом тысячелетии. Давайте поясню суть.

Все помнят советский сериал про Холмса? Титры там незабываемые…


Вот когда букв много – это содержимое памяти. Когда мы выделяем в памяти переменную структурного типа, мы как раз накладываем ту самую маску. Данные в памяти как лежали, так и продолжают лежать на тех же местах, но с точки зрения программы, они получают осмысленное значение. Причём я специально выбрал те титры, где есть поля «Роль», «Имя» и «Фамилия». То есть, полей у структуры много.

Теперь давайте вспомним несколько правил, на которых базируется язык Си:
  • для любой переменной может быть взят её адрес размещения в памяти,
  • указатель на элемент одного типа может быть преобразован в указатель на элемент другого типа,
  • имя массива и указатель на его первый элемент – это одно и то же.

Вот и прекрасно. Допустим, есть у нас некая функция pSerial->SendData,, которая принимает в качестве аргументов указатель на начало массива байтов и количество байт на отправку. Должны ли мы готовить именно массив байтов? Нет! Мы вполне можем разместить в памяти структуру. Я возьму пример из своей старой, но вполне реальной программы, без переделки. Там я не объявлял тип, а сразу разместил всё в памяти, так как структура использовалась всего один раз:
struct  
{
	WORD header;
	WORD msgID;
	float time;
	DWORD dwMode;
} alignData;


Теперь у меня есть переменная alignData, к полям которой я могу обращаться через точку. Ну я и заполняю их:
alignData.header = 0xa551;
alignData.msgID = 0x1111;
alignData.time = pMS->alignTime;
alignData.dwMode = pMS->alignInsAndMagMode;

И вот теперь начинается ловкость рук, и никакого мошенничества. Сначала я применю оператор & к имени переменной. У меня получится указатель на ту самую структуру (для которой даже нет типа, так что я не могу тут его написать, но он и не важен). Я тут же преобразую этот указатель к указателю на байты, написав (BYTE*). Да, программа старая, типы ещё использовались из заголовка <windows.h>. А этот указатель я вполне могу передать в функцию pSerial->SendData(). Итого получаем:
pSerial->SendData((BYTE*)&alignData,sizeof (alignData));

Давайте теперь я покажу то же самое, но единым фрагментом:
struct  
{
	WORD header;
	WORD msgID;
	float time;
	DWORD dwMode;
} alignData;

alignData.header = 0xa551;
alignData.msgID = 0x1111;
alignData.time = pMS->alignTime;
alignData.dwMode = pMS->alignInsAndMagMode;

pSerial->SendData((BYTE*)&alignData,sizeof (alignData));


Возвращаясь к аналогии с титрами, я сначала создал пустую маску:



Затем заполнил её поля:



После чего убрал маску, получив «сырое» содержимое памяти, над которой эта маска только что была. Как жаль, что фильм создавался в эпоху, когда титры рисовались вручную, поэтому эффектного рисунка создатели нам не оставили. Но давайте представим, что эта надпись где-то там есть…



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

2 То же, но в обратную сторону


В принципе, никто не мешает при приёме данных пойти точно таким же путём: создать в памяти переменную, типа «Структура», а её адрес передать в функцию, которая читает данные извне. Но именно перекачка данных в структуру не всегда будет оптимальной. Дело в том, что копирование данных – весьма медленная операция. А данных может быть много, например, если они пришли из сети. При этом, драйвер сначала принимает данные в системный буфер. Что с ними будет дальше – зависит от фантазии программистов, но встречал я случаи, когда при портировании библиотеки LwIp данные из буфера приёма сначала копировались в промежуточный буфер, а уже затем – в буфер прикладной части программы. Для быстрых систем это окажется приемлемым, но язык Си придумывали в эпоху, когда производительность ЭВМ измерялась сотнями тысяч операций в секунду. Не потеряла эта проблема актуальности и на современных дешёвых системах. Если можно не копировать данные, то лучше и не копировать их. Но как они тогда попадут в ту память, в которой размещена структура?

Чтобы решить эту задачу, создатели языка Си, также дали нам возможность наоборот привести указатель на массив к указателю на структуру. Кстати, в титрах именно так и делается. Берётся картинка и уже на неё накладывается маска.

Вот я нашёл какой-то чужой код, который принимает в качестве аргумента команду (явно тоже взятую из принятого массива) и указатель на буфер, в котором размещены какие-то данные:
int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData) 
{


Дальше начинается веселье. В зависимости от команды данные могут иметь совершенно разную структуру. И код, не копируя их никуда, просто рассматривает указатель на массив то как указатель на структуру LCD_X_SETORG_INFO:
    case LCD_X_SETORG: {
      //
      // Required for setting the display origin which is
      // passed in the 'xPos' and 'yPos' element of p
      LCD_X_SETORG_INFO * p;
      p = (LCD_X_SETORG_INFO *)pData;

      _LCD_SetOrg((int)LayerIndex, p->xPos, p->yPos);
      break;
    }


То как указатель на структуру LCD_X_SHOWBUFFER_INFO (имеющую совершенно другие поля):
    case LCD_X_SHOWBUFFER: {
      LCD_X_SHOWBUFFER_INFO * p;
      p = (LCD_X_SHOWBUFFER_INFO *)pData;

      _aPendingBuffer[LayerIndex] = p->Index;
      break;
    }


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

Ключевой момент здесь в том, что данные в каждую структуру не копировались. Мы просто рассматривали один и тот же буфер как разные виды данных. В этом примере копирование не заняло бы много времени, но был у нас проект, где мы убирали копирование целых сетевых пакетов, просто передавая указатель на системный буфер, куда этот пакет был принят, и там производительность «до» и «после» различалась очень сильно!

Давайте по традиции я покажу код в виде монолитного фрагмента:
int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData) 
{
…
    switch (Cmd) {
…
    case LCD_X_SETORG: {
      //
      // Required for setting the display origin which is
      // passed in the 'xPos' and 'yPos' element of p
      LCD_X_SETORG_INFO * p;
      p = (LCD_X_SETORG_INFO *)pData;

      _LCD_SetOrg((int)LayerIndex, p->xPos, p->yPos);
      break;
    }
…
    case LCD_X_SHOWBUFFER: {
      LCD_X_SHOWBUFFER_INFO * p;
      p = (LCD_X_SHOWBUFFER_INFO *)pData;

      _aPendingBuffer[LayerIndex] = p->Index;
      break;
    }
…
    case LCD_X_SETLUTENTRY: {
      //
      // Required for setting a lookup table entry which
      // is passed in the 'Pos' and 'Color' element of p
      LCD_X_SETLUTENTRY_INFO * p;
      p = (LCD_X_SETLUTENTRY_INFO *)pData;

      _LCD_SetLUTEntry((int)LayerIndex, p->Color, p->Pos);
      break;
    }


3 Вывод для языка Си


Итак. Чтобы отправить данные куда-то, их надо разложить в буфере по правилам, описанным той или иной документацией. Давайте возьмём первый попавшийся рисунок из документации на шину USB… Ну, скажем, формат команды для USB-флэшки.



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

Но вместо этого мы можем завести вот такую структуру:
typedef struct {
    uint32_t dCBWSignature;
    uint32_t dCBWTag;
    uint32_t dCBWDataTransferLength;
    uint8_t  bmCBWFlags;
    uint8_t  bCBWLUN;
    uint8_t  bCBWCBLength;
    uint8_t  CBWCB[16];
}msc_bbb_cbw;


И вместо переменных, хранящих те или иные параметры, хранить одну переменную. Тип переменной – эта структура. Ну всё равно же нам как-то надо значения хранить, почему не так? Но именно при таком хранении, компилятор сам (Сам! САМ!!!) разместит все поля так, что если мы возьмём адрес этой структуры в памяти, мы автоматически получим уже отформатированный массив под передачу!

В первом случае, мы отдельно храним, отдельно форматируем, во втором – мы всё так же храним, а форматирование за нас делает компилятор, просто потому, что мы ему рассказали, как правильно всё разложить по полочкам. В общем, мы к необходимой функции хранения, совершенно бесплатно получаем ещё и функцию форматирования.
Ну, а при обратном преобразовании тоже нам бесплатно на массив «странных» данных наложат ту маску, при которой мы будем видеть данные именно в том виде, в каком нам надо.

Ну замечательно же? И многие этим пользуются. Правда, многие, но не все. Если бы все пользовались, я бы не стал это всё писать. Поэтому после первичных разъяснений для всех, переходим к агитации для тех, кто этот механизм старательно игнорирует.

4 А что там на C++?


Однажды делали мы с напарником проект на C++. Вполне себе сетевой. И когда я собрался создать структуру, которая бы форматировала нужные нам данные, напарник начал читать мне лекцию, что так принято в этих ваших чистых Сях. А Плюсы, говорил он, объектно-ориентированный язык. Поэтому в нём надо делать функции сериализации и десериализации. Ну, то есть, как раз всё хранить в переменных-членах класса, никак не привязанных к передаваемому массиву, а по мере надобности – формировать тот массив и раскладывать всё по его байтикам.

Пришлось объяснить ему, что возможно, он и прав… Но даже если и так, то прав он только когда работает на большой ЭВМ. Мы же с ним делали проект на процессоре с 20 килобайтами флэша, не помню сколькими килобайтами ОЗУ (но понятно, что весьма скромный размер был) и работающим на частоте 48 мегагерц.

Для сериализации и десериализации пришлось бы писать код, который бы занял существенный объём памяти программ. Данные бы хранились дважды – в переменных-членах класса, и в передаваемом буфере, что создавало бы повышенную на грузку на ОЗУ (которого в контроллерах вечно не хватает). Ну, и наконец, процесс переноса занимал бы существенное время (в своей статье про DMA я показал, что доступ к памяти идёт ой как не за один такт). По иронии судьбы, в том проекте нам было велено пользоваться буферами без копирования данных именно для повышения быстродействия. А тут – расходы на преобразование.

Так что если вы работаете на ПК или ещё на какой быстрой машине, то можете идти на поводу у моды и использовать подход с сериализацией-десериализацией, копируя байтики. Но если система у вас простенькая – зачем тратиться на то, что и так получится бесплатно? Кто сказал, что сериализация обязательно должна гонять байты? Хотите сериализацию? Ну, сделайте вы ту самую переменную-член в виде структуры и работайте с ней. Все поля будут там заполняться сами, вы этого не увидите, но компилятор обеспечит это. А эта ваша сериализация пусть константы дозаполняют… Ой, для этого же конструктор придуман… Ну пусть контрольную сумму считает, да указатель на память возвращает. Так вы и за модой угонитесь, и производительность не просадите! А также сэкономите память программ и память данных. Последней на микроконтроллерах в сетевых приложениях вечно не хватает.

5 Хорошо, а что на Питоне?


Один наш Заказчик, который славится очень щепетильным подходом к оптимизации, прислал нам для ускорения работ свой код на Питоне. При этом он очень горячо сокрушался, что в отличие от кода на Сях, здесь приходится делать что-то, типа такого:
def to_byte(self):
        value = 0
        value |= (int(self.increment_level) & 0b111)
        value |= (int(self.reset_maybe) & 0b1) << 3
        value |= (int(self.rapid_mode) & 0b1) << 4
        value |= (int(self.unknown5) & 0b1) << 5
        value |= (int(self.unknown6) & 0b1) << 6
        value |= (int(self.yes_no_required) & 0b1) << 7
        return value


или такого:
    def to_bytes(self):
        return struct.pack("ABCD",
            self.magic,
            self.seed,
            self.leds.to_byte())


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

А тут, начав писать цикл статей, я понял, что это – отличный повод во всём разобраться и сделать практическую часть не на привычных мне С++, а на Питоне, с применением правильного подхода к формированию пакетов. И сам разберусь, и другим помогу это сделать!

В общем, есть в Питоне такая библиотека – ctypes. Вообще, как следует из описания, она придумана для того, чтобы можно было вызывать машинный код, размещённый в динамически компонуемых библиотеках (файлы *.dll для Windows и *.so для Линукса). А у этих библиотек аргументы могут быть представлены в виде структур. Правильных структур! Тех самых структур, про которые я писал выше, у которых положение полей должно отстоять от адреса начала на определённое, чётко заданное смещение!

Но где сказано, что эта библиотека должна использоваться только для вызова DLLек? Нигде! Мы вполне можем с её помощью организовывать структуры в памяти для личного пользования.

Структуры объявляются примерно так (надо заполнить переменную _fields_ до первого обращения к хранилищу):
class cluster(ctypes.Structure):
  _fields_ = [('nStars',ctypes.c_int),
    ('M_wd_up',ctypes.c_double),
    ('parameter',ctypes.c_double * 5),
    ('mean',ctypes.c_double * 5)]


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

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

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

6 Практическая часть


6.1 Общая подготовка


Ну что, поехали! Правда, я на Питоне пишу плохо, так что строго не судите, тут главное – идея.

Сначала добавим в пустой файл исходника необходимые библиотеки:
import argparse
import serial
import ctypes


Функцию main() я взял типовую, работающую с UART (да, я попросил у ChatGPT, чтобы он мне её создал). Вот её тело:
def main():
    # Настройка аргументов командной строки с помощью argparse
    parser = argparse.ArgumentParser(description="Send 'Hello World' over a COM port.")
    parser.add_argument("--COM_NUMBER", required=True, help="Specify the COM port (e.g., COM3)")
    args = parser.parse_args()

    # Получаем номер порта из аргументов командной строки
    port = args.COM_NUMBER

    # Настройки COM-порта
    baud_rate = 9600  # Скорость передачи данных
    bytesize = serial.EIGHTBITS  # 8 бит данных
    parity = serial.PARITY_NONE  # Без проверки четности
    stopbits = serial.STOPBITS_ONE  # 1 стоп-бит

    # Открываем COM-порт и отправляем данные
    try:
        with serial.Serial(port, baud_rate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=1) as ser:
            print("Message sent successfully.")
    except serial.SerialException as e:
        print(f"Error opening port {port}: {e}")
        exit(1)

if __name__ == "__main__":
    main()


6.2 Иерархия классов


Из первой статьи цикла мы уже хорошо знаем, что команды содержат поля, относящиеся к протоколу и поля, относящиеся к самой команде. К протоколу относятся адрес, субадрес, код команды и длина массива данных, идущие в начале посылки, а также контрольная сумма, расположенная в самом конце. Ну, а непосредственные данные уникальны для каждой команды.

Мне очень хотелось сделать так, чтобы постоянную часть не надо было размещать в каждом классе. С учётом, что проект писался на скорую руку, я решил эту задачу лишь частично. Что идёт в начале, то было вынесено в общий класс, а вот контрольную сумму пришлось добавлять ко всем классам. Увы.

Из кучи документов мы знаем, что в наследнике класса ctypes.Structure надо объявить переменную _fields_, чтобы описать поля структуры, которые будут храниться в объекте. ChatGPT говорит, что если надо сделать цепочку наследуемых классов, то в потомках следует писать что-то, типа:
class CommandWithSingleFloat (DpsCmd):
    _fields_ = DpsCmd._fields_ + [
        ("floatValue", ctypes.c_float),
        ("cs", ctypes.c_ubyte),
    ]


То есть, наращивать список. В принципе, он прав, но в частности – нет. Я провёл много экспериментов… Между получившимися списками вклинивается какое-то странное поле. Экспериментально удалось выяснить, что для устранения проблемы в классе-предке имя переменной должно быть каким-нибудь другим. Я использовал _fields1_ вместо _fields_. Тогда проблема уйдёт сама собой. Также стоит помнить, что у нас длина готовой структуры будет не кратна 32 битам, так что стоит установить упаковку данных с точностью до байта, чтобы не происходило автоматических выравниваний.

С учётом всего, вышесказанного, общий предок для всех команд, объявляется так:
class DpsCmd (ctypes.Structure):
    _pack_ = 1
    _fields1_ = [
        ("cmd1", ctypes.c_ubyte),
        ("cmd2", ctypes.c_ubyte),
        ("cmd3", ctypes.c_ubyte),
        ("dataLen", ctypes.c_ubyte)
    ]


Чуть позже мы добавим ему функционала. А пока, опишем потомков. Я предлагаю сделать иерархию следующей:



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

6.3 Классы второго уровня иерархии


Вот и прекрасно. Давайте сделаем вот такой класс, который передаёт команды в формате float:
class CommandWithSingleFloat (DpsCmd):
    _pack_ = 1
    _fields_ = DpsCmd._fields1_ + [
        ("floatValue", ctypes.c_float),
        ("cs", ctypes.c_ubyte),
    ]


И всё! И даже не надо никакого функционала! Есть предок, первые байты там будут положены в переменную-член _fields1_. Ещё раз напоминаю, что единица в имени выстрадана. Она устраняет проблемы на стыке. Здесь мы формируем переменную _fields_ (уже без единицы) из общих и персональных полей. Причём поле данных у нас будет типа ctypes.c_float.

Класс передающий байтовые параметры, кроме имени, будет отличаться буквально одной строкой (надеюсь, вы найдёте её самостоятельно):
class CommandWithSingleByte (DpsCmd):
    _pack_ = 1
    _fields_ = DpsCmd._fields1_ + [
        ("byteValue", ctypes.c_ubyte),
        ("cs", ctypes.c_ubyte),
    ]


6.4 Добавляем функционал в класс первого уровня иерархии


Пора проверять идею, всё ли работает верно. Я это делал на примере функции расчёта контрольной суммы. Вы можете повторять всё такими же короткими шагами, вставляя после каждого из них отладочную печать. Итак, добавляем в класс DpsCmd функцию:
def CalcCheckSum(self):


Интересно, будут ли размеры структур соответствовать действительности? Скорее, чтобы получить ответ на этот вопрос, чем по архитектурным соображениям, я стал заполнять поле структуры, содержащее длину, в этой функции. Так появилась первая строка, с которой я очень долго экспериментировал:
def CalcCheckSum(self):
    # Заполнили поле с длиной команды, в зависимости от длины структуры
    self.dataLen = ctypes.sizeof(self)-5


Создав объекты типов CommandWithSingleFloat и CommandWithSingleByte и вызвав их функцию CalcCheckSum(), я, глядя на временно добавленный отладочный вывод, понял, что всё верно. Размер у структур получается такой, какой требуется. Ну, точнее, вначале всё было плохо, но когда я добавил ту самую единичку к имени _fields1_, всё наладилось.

Теперь для расчёта суммы, нам нужен указатель на байтовое представление структуры. Для этого, добавляем строку:
# Получили указатель на память, где структура лежит
  byte_ptr = ctypes.cast(ctypes.byref(self), ctypes.POINTER(ctypes.c_ubyte))


С этого момента у нас есть прекрасный массив, из которого мы можем читать данные и писать их в него обратно. При этом данные будут обновляться и в полях структуры. Ну, и изменение полей структуры тут же будет отражаться в массиве. Огромный простор для разных экспериментов с одновременным отладочным выводом! Правда, в реализации самого функционала есть одна маленькая проблема. Как выяснилось, тип c_ubyte не поддерживает операции сложения. Читать из него можно, писать – тоже. А вот складывать две таких переменных – нельзя. Поэтому сумму мы копим в обычной переменной типа int:
        # uByte не имеет операции сложения, поэтому
        # будем копить КС в переменной типа int
        cs = 0
        # Бежим по всем байтам, которые подлежат расчёту
        for i in range (2,ctypes.sizeof(self)-1):
            cs += byte_ptr[i]


А когда всё рассчитано – кладём результат в поле структуры:
        # Отбросили старшие байты суммы и сохранили в структуре
        self.cs = cs & 0xff


Собственно, всё! Полное тело функции будет выглядеть так:
def CalcCheckSum(self):
   # Заполнили поле с длиной команды, в зависимости от длины структуры
   self.dataLen = ctypes.sizeof(self)-5

   # Получили указатель на память, где структура лежит
   byte_ptr = ctypes.cast(ctypes.byref(self), ctypes.POINTER(ctypes.c_ubyte))

   # uByte не имеет операции сложения, поэтому
   # будем копить КС в переменной типа int
   cs = 0
   # Бежим по всем байтам, которые подлежат расчёту
   for i in range (2,ctypes.sizeof(self)-1):
       cs += byte_ptr[i]
   # Отбросили старшие байты суммы и сохранили в структуре
   self.cs = cs & 0xff


6.5 Ещё одна функция первого уровня иерархии


Чтобы получить красивый код, я решил в этот же класс DpsCmd поместить и функцию отправки в COM-порт. Если бы проект был большим, я бы сто раз подумал, делать так или нет, но для тестового проекта получилось очень даже забавно. Будем считать, что это и есть функция сериализации, о которой я тут так много рассуждал. Ссылку на объект uart приходится передавать в неё в качестве аргумента. Для отправки данных в UART массивом не обойтись, здесь приходится пользоваться формированием строки, на основе памяти, в которой размещена структура. Для этого будем использовать функцию ctypes.string_at(). В итоге, новая функция выглядит так:
 def SendToUart (self, uart):
     self.CalcCheckSum()
     data_to_send = ctypes.string_at(ctypes.byref(self), ctypes.sizeof(self))
     uart.write (data_to_send)


6.6 Классы третьего уровня иерархии


Ну вот, когда основной функционал готов, давайте сделаем классы, относящиеся к конкретным командам. Честно говоря, мне больше всего нравится посылать команду установки напряжения. Так получилось, что именно её я нашёл первой, так давайте её первой и отправим. Делаем класс:
class cmdSetVoltage (CommandWithSingleFloat):
    def __init__(self, voltage):
        super().__init__(0xf1, 0xb1, 0xc1)
        self.floatValue = voltage


Она вызывает предка, задав ему конкретный код команды (из таблицы, приведённой во второй части статьи). А требуемое напряжение она помещает в поле уже отдельно. Теперь можно разместить объект в переменной, а затем вызвать эту функцию, а можно вызвать её без создания переменных с объектом, написав внутри функции main() так:
cmdSetVoltage (3.49).SendToUart(ser)


Почему 3.49? Так в первой части статьи мы устанавливали именно это значение в фирменной программе и получили такой лог:
OUT    f1 b1 c1 04  29 5c 5f 40  e9


Вот и прекрасно! Запускаем сниффер COM-порта, а затем – наш питоновский код. И видим в логах…



Ура! Всё работает!

Из второй части статьи известно, что сразу после открытия порта следует послать команду CMD_1. Давайте скорее опишем класс, формирующий её:
class cmdFirstCommand (CommandWithSingleByte):
    def __init__(self):
        super().__init__(0xf1, 0xc1, 0x00)
        self.byteValue = 1;


Теперь в функции main() будет сразу две команды:
   cmdFirstCommand().SendToUart(ser)
   cmdSetVoltage (3.49).SendToUart(ser)


Посылаем их… Иииииии… Вуаля! Вот фотография экрана блока:



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

7 Заключение


В статье показано, что структуры – это мощное средство для обработки потоковых данных. Структуры легко могут быть преобразованы в массив под передачу, а принятый массив не менее легко может быть разобран в виде структур. Это касается не только языков С/C++, но даже языка Python, при использовании библиотеки CTypes.
Источник: https://habr.com/ru/articles/875696/


Интересные статьи

Интересные статьи

Добавляем функционал быстрых ответов на любом тарифе Битрикс24Как добавить одну из самых полезных функций для чатов открытых линий для любого коммерческого тарифа? Все очень просто, нужно лишь..... ...
На старте карьеры в IT меня очень волновал этот вопрос. Настолько волновал, что в первой же команде, куда я попал, я стал изучать стек технологий, который использовала команда. Начал писать какой-то п...
В этой статье мы популярно объясняем на собственном опыте как организовать массовую выгрузку, обработку и загрузку фотографий товаров из Bitrix, используя Python и минимальное количество SQL. Для проч...
Большинство людей думают о нёрдах (ботанах) как о тихих и застенчивых людях. В обычных социальных ситуациях они такие же тихие и застенчивые, какими был бы звёздный квотербек, оказавш...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.