Общий вопрос по программированию ПЛК в CODESYS.
Хотя проект выполнен на другом вендоре, есть перспектива переноса на ПЛК ОВЕН.
Также, т.к. вопрос общий, думаю он может быть интересен всем, кто работает на CODESYS.
Потому и на вашем форуме решил скромно попросить совета.

Проект уже написан и крутится на объекте, но есть кривой момент, который хочется устранить.
Кроме основной проблемы, буду рад любым советам по описанной далее структуре данных,
т.к. это моя первая большая программа, причём сразу для резервированного ПЛК.
Были маленькие программы, был небольшой опыт на языках программирования общего назначения.
Был синий пояс на codewars Ну и n-ное количество лет электромонтером и инженером. Предложения о работе в личку

ПЛК Эмикон, резервированный, из особенностей: отсутствие аппаратной части в проекте, нет поддержки RETAIN.
Вместо этого статическая меркерная память и отдельный конфигуратор, что не суть, т.к. не в этом проблема.
По ссылке внизу выложен архив проекта, точнее кусок, выдранный из реального проекта, чтобы показать проблему.
Можно запустить через эмулятор Эмикон, среду CONT-Designer и полный импорт проекта, если кто-то знаком c этим вендором.
Если нет, не советую ставить их среду, эмулятор или вообще пользоваться данным ПЛК Откройте проект в любом CODESYS, ничего не импортируйте, далее вы можете видеть код, при желании скопировать его в рабочий проект на вашей среде, с которой работает ваш эмулятор.

Были применены элементы ООП, ради удобства и чтобы не дублировать код.
Проект написан смесью CFC и ST, но CFCшная часть в представленном куске минимизирована до голых вызов ФБ.
В реальном проекте, это не только вызовы ФБ, но и возможность управлять объектом онлайн, имея CFC диаграммы в качестве мнемосхемы на втором мониторе, при ПНР это удобно. Там расписаны входа-выхода, чтобы всё отображалось.

Описание проекта.
- Есть Basic_FB, содержит поля, нужные всем, абстрактный.

- Есть абстрактные ФБ (DE_Basic, RVS_Basic) агрегатов, в примере: дренажные емкости DE и резервуары RVS - наследующие Basic_FB.
В этих абстрактных ФБ расписан набор методов с модификатором protected, без аргументов, работающих напрямую с полями ФБ.
Переход на работу через аргументы ничего не изменит в проблеме, скажу сразу, т.к. дело не в самих аргументах, а в их типе.

- Есть конкретные ФБ агрегатов, наследующие абстрактным ФБ своей группы, т.е. ФБ DE1, DE2 наследуют DE_Basic. Им доступны все поля и методы родителя, своих полей они не имеют, разве что отладочные поля или при появлении специфического для данного агрегата функционала. Создаются в программе в единственном экземпляре. Т.е. это так называемые синглтоны, но без программного.
Методы родителя могут быть переопределены, если агрегат вдруг стал чем-то отличаться от остальных.

- Есть отдельные функции (хелперы), но разумеется они могут выполнять лишь часть функционала, который точно одинаков для всех.

- Есть структуры данных, свои для каждой группы агрегатов, лежат в папке DUT. Все структуры этого куска проекты имеют один тип внутри, чтобы их объединить в Union с таким же массивом и иметь сразу и возможность работы с циклом через индексы, и доступ по имени через точку. Там же есть папка Default, для таких структур с уставками по умолчанию, но без Union, т.к. эти структуры просто копируются в рабочие при необходимости.

Там же есть папка Unions, содержащая юнионы, в каждом юнионе два поля, strt и arr.

Структуры тоже используют наследование. Структуры вида ***_Sensors содержат REAL поля для датчиков, а все структуры с уставками наследуют структуре вида ***_Sensors, автоматически получая тот же набор полей. Есть структура Flags, где те же самые поля, но типа WORD, чтобы взводить в них биты, связанные со срабатыванием уставок. Наконец, ***_SETPOINTS_RETAIN содержит полями все Unions уставок, чтобы всё это разом можно было синхронизировать между ведущим и ведомым ПЛК через статическую память. Т.е. в ФБ работа ведётся именно через эту структуры, а вся остальная иерархия нужна, чтобы быть объявленной в ней.

Код:
{attribute 'pack_mode' := '2'}
TYPE RVS_Sensors :
STRUCT
	Tempr					:REAL;
	Level					:REAL;
	empty3					:REAL;
	empty4					:REAL;
	empty5					:REAL;
	empty6					:REAL;
	empty7					:REAL;
	empty8					:REAL;
	empty9					:REAL;
	empty10					:REAL;
END_STRUCT
END_TYPE
Код:
{attribute 'pack_mode' := '2'}
TYPE RVS_Presets_HH EXTENDS RVS_Sensors:
STRUCT
END_STRUCT
END_TYPE
Код:
{attribute 'pack_mode' := '2'}
TYPE RVS_Flags :
STRUCT
	Tempr					:WORD;
	Level					:WORD;
END_STRUCT
END_TYPE
Код:
{attribute 'pack_mode' := '2'}
TYPE uRVS_Sensors :
UNION
	strt			:RVS_Sensors;
	arr			:ARRAY[0..CONST.RVS_Sensors_Count] OF REAL;
END_UNION
END_TYPE
Для индекса массивов применены константы, позволяющие задавать длину в одном глобальном списке. В структурах заложены пустые поля, которые можно отрезать, указав длину юнион-массива меньше, чем количество полей структуры. Всё довольно просто, это как птица, тело которой - основные структуры, левое крыло - юнион с массивом для расширения функционала, правое крыло - дефолтные значения уставок. А, сама птица посажена в "клетку" вида ***_SETPOINTS_RETAIN, чтобы её переносить, т.е. для синхронизации памяти. Напомню, что это кусок проекта, в рабочем я ещё и STATE также синхронизирую, куда входят команды, флаги, и.т.п. Т.е. в представленном куске проекта есть артефакты, которые задействованы в полном проекте.

Всё, структура данных и наследование, в двух словах, описаны. Кто сказал, что программирование это сложно?

Проблема.
В каждом абстрактном агрегатном ФБ: DE_Basic, RVS_Basic, а в реальном проекте их конечно больше - есть полностью одинаковые методы. В данном куске оставлен метод AI_check(). Приведу его код, хотя сам код тут не имеет никакого значения. Он работает с полями ФБ, в котором объявлен, т.к. они ему доступны непосредственно. Экземпляры, получив всё от родителей, имеют свои уникальные "копии" всего этого функционала.

Код:
METHOD PROTECTED AI_check : BOOL
VAR
	i					:DINT;
END_VAR

FOR i:= LOWER_BOUND(Sensors.arr, 1) TO UPPER_BOUND(Sensors.arr, 1) DO
	
	IF LIMIT_LOW(Sensors.arr[i], Presets_LL.arr[i]) THEN BIT_IN_WORD( Flags.arr[i], 1); END_IF

	IF CHECK_LOW(Sensors.arr[i], Presets_Low.arr[i], Sensors_Hyst.arr[i] )
	THEN BIT_IN_WORD( Flags.arr[i], 2);
	ELSE ZERO_IN_WORD( Flags.arr[i], 2);
	END_IF
	
	IF CHECK_HIGH(Sensors.arr[i], Presets_High.arr[i], Sensors_Hyst.arr[i])
	THEN BIT_IN_WORD( Flags.arr[i], 3);
	ELSE ZERO_IN_WORD( Flags.arr[i], 3);
	END_IF

	IF LIMIT_HIGH(Sensors.arr[i], Presets_HH.arr[i]) THEN BIT_IN_WORD( Flags.arr[i], 4); END_IF
	
END_FOR
Код методов полностью одинаковый из-за того, что везде применены одни и те же имена, т.е. идентификаторы для экземпляров пользовательских типов, объявленных в абстрактных ФБ. Но, типы разные, хотя имена те же!!!

Поэтому, нет возможности вынести эти одинаковые методы на уровень выше, т.е. в Basic_FB. Конечно, что просто их туда скопировать, они и поля "видеть" перестанут, т.к. в Baisc_FB, ни Sensors, ни Flags ещё не объявлены, т.е. на этапе компиляции они недоступны. ИХ можно передать аргументами, но аргументы какого типа мы будем принимать? Верно, один для одного, другой для другого, работать так не будет.

Решение которое я знаю - применение низкоуровневого копирования. Но, если для Эмикон это можно считать нормой, для того же Regul, например, уже нельзя. Как тогда решить проблему?
Есть информация о том, как организовать ООП полиморфизм для стандартных типов, хоть runtime, хоть compiletime.
Но, здесь типы пользовательские, утрачивать имеющуюся архитектуру данных не хотелось бы, да и не ясно, как всё это вообще организовать человечески, без применения структур?

В языках общего назначения есть функционал типа Generic'ов, хотя я очень поверхностно с этим знаком, который вроде как позволяет делать именно это, т.е подставлять нужный тип. Хотя, повторюсь, могу ошибаться. В Codesys такого функционала нет.

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

Прошу у форума помощи в решении озвученной проблемы.

https://disk.yandex.ru/d/ezVjYTNgdu0Dzw