PLExt: выражения вместо клубка проводов и блоков

  • polygon
  • plext
  • fbd
  • плк
  • c++

Полигон:

графическая среда разработки программ для промышленных контроллеров на языке функциональных блоков. Страница производителя, ООО «ПромАвтоматика-Софт» и раздел среды Полигон на сайте ОВЕН.

Так получилось, что я довольно плотно занялся изучением этой системы, чтобы в кратчайшие сроки завершить один проект на базе контроллеров ПЛК210 с Полигоном на борту. Система очень интересная, позволяет программировать контроллеры на своей версии FBDFunction Block Diagram — графический язык программирования контроллеров, определённый [IEC 61131-3](https://webstore.iec.ch/en/publication/68533). и довольно успешно, хоть и не обросла еще "взрослыми" фичами и плюшками, типа системы управления переменными, встроенными журналами и прочим таким. Есть фбд блоки, которые с разной степенью успешности реализуют этот, или близкий функционал, но говорить о комфортной среде, которая даст готовые переменные и логи, нельзя. Но есть рабочая реализация OPCUA клиента и сервера (само это уже очень много), есть достаточная базовая библиотека, есть модбас, поэтому жить можно. Если не уходить далеко от натоптанных дорог, то система будет стабильна и подводить не будет. Но все же, в базовой библиотеке не хватало функционала, который я довольно самоуверенно взялся реализовать с помощью кодекса.

Сразу нужно сказать, что интерфейс у Полигона специфичный и требует привыкания. Все сделано в одном большом окне (привет Мастерскаде еще сразу передать захотелось), поэтому без большого монитора все очень тоскливо. Плюс внутри окна управляются скорее как в wmii или i3, чем в винде, так что тем, кто с этими оконными менеджерами не работал будет непривычно. С клавиатурой, напротив, все как в винде, ее мало и тоскливо, все приходится мышкой тыкать.

Быстро стало понятно, что интерфейс для разработки блоков на C++ довольно кривой и руками добиться от него чего-то рабочего довольно сложно. Я начал реверсить формат проекта pl2 и библиотеки ll2Файл библиотеки Polygon с типами, интерфейсами и кодом функциональных блоков. из любопытства и через некоторое время получил некоторый набор правил, с которыми удалось начать делать C++ блоки почти без проблем. Этот реверс превратился в набор тулзов, которые я до сих пор отлаживаю, но которые уже показывают более менее стабильный результат и даже позволяют помечтать о вайб-кодинге, хотя до этого еще далеко. Одним из первых блоков, которые получились рабочими, в этих экспериментах, оказались блоки выражений, которые я подсмотрел у коллег в проектах под Шнайдер. На первую версию этих блоков у меня прав нет, но я могу сделать еще одну реализацию той же идеи с нуля, с новым кодом, новой архитектурой и чуть другим подходом.

PLExt

Итак, я сделал отдельную библиотеку блоков для Polygon — PLExt. Сейчас в ней 11 функциональных блоков, из которых пять относятся к семейству Expression* и я продолжаю ее развивать, идей пока много.

Все блоки Expression - это обертки одного ядра под разные типы данных. На вход принимают выражение и переменное число входов, и вычисляют результат на выходы. Выходов тоже может быть много. Блоков пришлось делать пять, потому что текущая версия полигона не умеет хорошо работать с переменным количеством входов и выходов разных типов. Если бы умела, это все можно было бы объединить в один красивый блок, у которого могли бы быть входы и выходы разных типов под любые задачи.

Тем не менее и с одним фиксированным типом получился достаточно мощный инструмент: сложная математика и логика стали выглядеть читаемо и коротко, не загромождая лист десятками блоков и проводов.

Состав PLExt в дереве библиотек Polygon
Состав PLExt в дереве библиотек Polygon

Пять вариантов Expression*, преобразователи строк и транспортные блоки для тестов.

Одно ядро для пяти блоков

У всех Expression* общее ядро. Это один компилятор и исполнитель выражений с пятью адаптерами Polygon.

Поддерживаются типы:

b — bool
i — int32
u — uint32
f — float
s — string

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

Входы обозначаются как iN, выходы — как oN. Несколько операторов разделяются точкой с запятой:

o0 = i0 + i1;
o1 = i0 - i1;
o2 = i0 * i1;

Количество контактов задаётся интерфейсом экземпляра и во время работы не меняется. На ходу можно менять текст формулы и значения входов.

Если нужен только o0, присваивание можно опустить:

i0 > 10 ? i0 * 2 : 0

Язык C-подобный, но с реверансами к Питону и Руби. Поддерживает арифметику, сравнения, битовые операции, сдвиги, степень **, математические функции и преобразования типов. Операторы &&, || и ?: вычисляются лениво, поэтому условием можно закрыть недопустимую операцию, например деление на ноль.

Несколько выходов и временные переменные

Одна программа может вычислить общую часть и разложить результат по нескольким выходам:

v0 = i0 - i1;

o0 = v0 * i2;
o1 = v0 + i3;
o2 = v0 < 0 ? -v0 : v0;

vN — временные переменные текущего запуска. Их тип выводится автоматически по присвоенному значению. Они позволяют не повторять общие части формулы и разбивать длинное вычисление на короткие шаги.

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

Состояние между циклами

Переменные wN сохраняют значение между циклами. При первом использовании задаются начальное значение и тип:

w0(0,i) = w0 + i0;
o0 = w0;

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

Целочисленные выражения и состояние между циклами
Целочисленные выражения и состояние между циклами

Верхний блок формирует четыре результата из одной программы. Нижний использует сохраняемую w0 и выполняется на каждом цикле.

Если wN нет, программа исполняется на первом цикле, при изменении формулы или значения входа. При неизменных данных готовые выходы сохраняются без повторного вычисления.

Если в формуле встречается хотя бы одна wN, на каждом цикле исполняется вся программа. Ограничиться оператором с прямым обращением к wN нельзя: зависимость может проходить через vN или oN.

Проверка ExpressionFlt и ExpressionBool в Polygon
Проверка ExpressionFlt и ExpressionBool в Polygon

Математика и составная логика. Входные значения подаются через проводные соединения.

Строковые выражения

Строки выросли из той же задачи сокращения графической схемы. Для записи в журнале, сообщения об аварии или значения с единицей измерения в FBDFunction Block Diagram — графический язык программирования контроллеров, определённый [IEC 61131-3](https://webstore.iec.ch/en/publication/68533). иначе понадобится цепочка преобразований, форматирования и конкатенации.

В ExpressionStr строка собирается одним выражением:

o0 = f"Температура: {i0:.1f} °C";

Можно сформировать несколько строк:

o0 = f"Температура: {i0:.1f} °C";
o1 = f"Уставка: {i1:.1f} °C";
o2 = i0 > i1 ? "Выше уставки" : "Норма";

Поддерживаются обрезка пробелов, длина в символах UTF-8, индексация и срезы, поиск, регистр с кириллицей, русская транслитерация, ограниченные sprintf и fmt, а также шаблоны f"..." (Привет, Питон).

Проверка ExpressionStr
Проверка ExpressionStr

UTF-8, срезы, транслитерация и форматирование. Один блок формирует шесть строковых выходов.

Строки Polygon и OPCВнешняя программа, которая читает или записывает опубликованные контроллером переменные, например SCADA или диагностическое приложение.

Самые неприятные проблемы возникли не в парсере, а при публикации строк наружу.

Первый вариант отдавал char* из std::string::c_str(). Но выяснилось, что у этого типа функция записи вообще ничего не делает и поменять значения через OPCВнешняя программа, которая читает или записывает опубликованные контроллером переменные, например SCADA или диагностическое приложение. не получится, а значения на выходах из-за времени жизни указателя не стабильны.

По блокам в стандартной библиотеке можно догадаться, что пользоваться лучше типом st::STRING. Этот тип владеет буфером, редактируется средствами Polygon и подходит для проводных соединений.

Однако общей блокировки между записью переменной в цикле и параллельным чтением через OPCВнешняя программа, которая читает или записывает опубликованные контроллером переменные, например SCADA или диагностическое приложение. в Polygon нет, поэтому OPCВнешняя программа, которая читает или записывает опубликованные контроллером переменные, например SCADA или диагностическое приложение. (а через него реализована и отладка в Полигоне и межконтроллерное взаимодействие и со скадой удобно, если поддерживает) может прочитать строку прямо посреди ее обновления, получив обрезанную битую строку. К тому же, иногда обрезанную посреди символа UTF-8, русской буквы, например, что, кажется, приводит к падениям и переподключениям клиента. Поэтому пришлось минимизировать количество перезаписей строк, и даже придумать простой протокол, чтобы с большей вероятностью получать исправные строки ready/updated.

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

updated == ready

означает, что снимок опубликован полностью. Ненулевой номер обрабатывается один раз; ready == 0 включает автоматический режим.

Повторюсь, ready/updated не блокирует память. Это протокол между блоком и сотрудничающим клиентом, который позволяет не читать набор строк в процессе публикации.

Ошибки и err_seq

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

Дополнительно у каждого Expression* есть счётчик err_seq. Нечётное значение означает запись строки, чётное — устойчивое состояние. Машинный клиент может прочитать счётчик, скопировать err и принять результат, только если второе чтение вернуло то же чётное значение.

Это не общая блокировка st::STRING, но она сокращает число перезаписей и позволяет обнаружить пересечение чтения с обновлением.

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

FBD
Function Block Diagram — графический язык программирования контроллеров, определённый IEC 61131-3.
CFC
Continuous Function Chart — распространённое расширение FBD со свободным размещением элементов. CODESYS указывает, что CFC не является отдельным языком IEC 61131-3.
LL2
Файл библиотеки Polygon с типами, интерфейсами и кодом функциональных блоков.
цикл контроллера
Повторяющееся чтение входов, выполнение программы и обновление выходов.
OPC-клиент
Внешняя программа, которая читает или записывает опубликованные контроллером переменные, например SCADA или диагностическое приложение.