Взаимодействие с кодом на C/C++ =============================== .. _c: .. _cppyStructURL: https://github.com/karkason/cppystruct Нам повезло, что формат записи данных, компилируемые для классических архитектур, на Python и C совпадают. Это позволяет очень легко и эффективно организовывать обмен между Python и C. Это также позволяет нам работать с удалённой системой, написанной на С. И эффективно и быстро переносить разработанный в Python код на встраиваемые системы - сначала компилируя и тестируя его на собственном компьютере, а потом и под архитектуру процессоров удалённого оборудования. Если вы дошли до этого места документации, то, скорее всего, у вас не будет проблем в понимании кода модуля и базы данных на Python. .. Поэтому, расскажу только о особенностях кода на C и особенностях организации взаимодействия модулей Python - C/C++. Проще всего продемонстрировать эту возможность на примере. В директории проекта пример для C++ находится в директории ex5.1 (с использованием `библиотеки cppyStruct `_), для ANSI C - в директории ex5.2. Код на C ~~~~~~~~ В примере реализована одномерная модель вращательного движения космического аппарата. КА управляется контроллером, в котором используется закон управления BDot. Он позволяет эффективно гасить угловую скорость космического аппарата. На стороне Python решается дифференциальное уравнение вращательного движения КА во времени. Структура кода стандартная, состоит из двух файлов - собственно модель и база данных. Если вы посмотрите файл базы данных, видно что в ней стандартным образом прописаны выполняемые модули (словарь Tasks), где для модуля на C жёстко зафиксирован сетевой адрес. Привязка адреса сделана для того, чтобы стандартизировать и облегчить компиляцию C-кода. Больше никаких особенностей тут нет. Со стороны C программа состоит из нескольких модулей. Основой является главный файл pySimModule.c с функцией main. В нём написана простая модель состояний, которая реализует алгоритмы управления задачами. Здесь реализованы инициализация, расчётный шаг и финализация задачи. Здесь же производится запуск программы контроллера, которая находится в файле ctrl.c. Для обмена пакетами написан упрощённый коммуникатор (communicator.c), который позволяет обмениваться udp-пакетами между Python и C. Причём структура пакета жёстко привязывается к коду на этапе компиляции (в файле db.h), что удобно для отладки и уменьшает объем кода для встраиваемых систем. В Python при создании задачи можно автоматически сгенерировать файл db.h, который описывает структуру базы данных, используемой в проекте и, соответственно, задаёт формат пакета данных. Коммуникатор принимает пакет и раскладывает данные в структуру с полями, имена которых соответствуют именам переменных. Эта структура используется в основной программе. Вот пример файла db.h .. code-block:: c // База данных. !Файл создаётся автоматически, не редактируйте его! //import libKernel, db //class Controller(libKernel.TaskTemplate): // pass //model = Controller(TaskList=db.Tasks, DB=db.DataBase()) //model.Manager.genCHeader("path to db.h") #ifndef DB_H_ #define DB_H_ #include #include // Для типа uint8_t #define DB_SIZE 32 // размер буфера БД typedef uint8_t DbBuf_t[DB_SIZE]; typedef struct { float t; int cmd; float B[3]; float U[3]; } DB_t; #define COM_ADDRESS "127.0.0.1" #define COM_PORT 6502 #endif /* DB_H_ */ В предваряющем комментарии приведен код на Python, который сформирует этот файл. .. note:: В настоящей версии используются только типы float, int и массивы этих типов. Алгоритм BDot реализован классически: .. math:: U = -k \dfrac{\dot{B}}{\Vert B \Vert}. Код контроллера на Python: :: class Controller(libKernel.TaskTemplate): kBDot = -20 def BDot(self): """ алгоритм B-Dot по нормализованному вектору МПЗ """ Bn = self.B/np.linalg.norm(self.B) dB = Bn - self.Bnp dt = self.t - self.tp self.U = np.clip(self.kBDot * dB/dt, -1., 1.) self.Bnp = Bn self.tp = self.t def Initialize(self): self.Bnp = np.zeros(3) self.tp = -0.1 def Run(self): self.BDot() А вот код контроллера на C: .. code-block:: c void BDot(DB_t *db){ // алгоритм B-Dot, честный трёхмерный static float BNp[3]={0,0,0}; static float tp=0; float dt, m; float dBN[3], BN[3]; dt = db->t - tp; // нормализуем B m = 0; for (unsigned char i = 0; i < 3; i++) m = m + SQR(db->B[i]); m = sqrt(m); for (unsigned char i = 0; i < 3; i++) { BN[i] = db->B[i]/m; // сразу же считаем dB/dt = (BN-BNp) / dt, всё нормализованное dBN[i] = (BN[i] - BNp[i])/dt; // управление db->U[i] = -20.*dBN[i]; db->U[i] = limit1(db->U[i]); // и сохраняем на след итерацию BNp[i] = BN[i]; } tp = db->t; // время тоже сохраняем на след итерацию } Здесь расчёт по формуле немного оптимизирован - компоненты вектора управления рассчитываются в одном цикле. Но можно непосредственно видеть, что алгоритм идентичен - он даёт одинаковый переходной процесс. .. image:: img/BDotC.png :alt: Переходной процесс Переходной процесс гашения угловой скорости алгоритмом B-Dot. Можно запускать либо модуль на Python, либо программу на C. Одновременно не получится. Код на C++ ~~~~~~~~~~ Внимание: надо переструктурировать документацию. Этот пример находится в каталоге ex5.4. Так как C и C++ очень близкие языки, они (в целом) совместимы между собой по архитектуре программ и структурам данных. Основное отличие - в механизме разрешения имен функций и перегрузки операторов, характерных для C++. Поэтому достаточно просто использовать код C для C++, который мы написали в только что разобранном примере. Для большего эффекта, код C++ выполнялся на внешнем процессоре - ESP8266 D1 mini, который имеет встроенный WiFi приемопередатчик. Код был написан в Arduino IDE. Как и в примере выше, для обмена данными используются те же UDP-пакеты, и данные в памяти хранятся одинаково (представления структур в C и C++ для архитектур ESP и x86 совпадают побайтово) - это позволило сохранить логику обработки пакетов. Удалось передавать запакованную базу данных и распаковывать её в структуру простым побайтовым копированием данных. Точно так же реализована и машина состояний расчётного модуля, заимствованная из предыдущего проекта. Чтобы линкер находил функции C (у нас одна - BDot) в заголовочном файле используется квалификатор "extern": .. code-block:: c #ifdef __cplusplus extern "C" { #endif // Функции и структуры языка C #ifdef __cplusplus } #endif Обмен данными производится UDP-пакетами, механизм работы с которыми предоставляет модуль WiFiUdp. Данные пакета, запакованного на стороне Python функцией struct.pack, разворачиваются в структуру db простым копированием: .. code-block:: c udp.read((byte *)&db, DB_SIZE); // Читаем весь пакет в структуру db При этом структура db представляется как байтовый массив. Обратная сборка пакета производится аналогично. В результате работы программы мы получаем тот же самый график, который вы видели только что в предыдущем примере. Понятно, что на внешних устройствах можно не только обрабатывать данные, но и получать информацию с датчиков и выдавать управляющие воздействия на привода, выполнять сложные алгоритмы типа калибровки датчиков. Автор надеется, что приведённый код поможет вам реализовывать проще свои проекты.