Поводом для написания статьи послужило не очень приятное для меня событие: модератор Хабра убрал теги – «С++» и «Параллельное программирование» из моей крайней статьи [1]. Этому предшествовало сообщение пользователя, который по его словам не заметил в статье ни С++, ни параллелизма и поспешил об этом известить весь свет. На самом деле он, скорее всего, просмотрел статью по диагонали и попросту "не врубился". Другим объяснить сей казус сложно. Я объяснил причины его заблуждения, но это не было принято во внимание. В ответ – тишина и, более того, пошли у него на поводу.
А до этого, но тоже в контексте рассматриваемой статьи (теперь все это складывается в один пазл), произошло еще одно событие. Другой пользователь, решивший, видимо, поддержать автора, выразил благодарность, но допустил не очень лестные высказывания в адрес специалистов Бауманки. Не то, чтобы я с ним был во всем согласен, но поскольку на асфальте розы не растут, то в чем-то он был все же прав.
Но дело даже не в содержании постов. Если раньше управлять спорными постами доверялось автору, то теперь модератор решил взять на себя это право. Может, у нас месячник усиленной модерации? Но только в чем причины столь сильного недоверия автору? Ведь, модератор явно не очень вникал в суть проблемы и причины появления подобных постов. Логично было бы предоставить, как и ранее, автору решать вопросы, связанные с содержанием его же статей. Последнее сообщение, хотя и содержало критику, но в целом не нарушало правила сообщества. Хотя, обобщать на всю Бауманку, конечно, не стоило бы.
Однако, оставим пересуды в стороне, а поговорим про С++, параллелизм и об автоматном программировании. А тут без ВКПа и, как ни странно, без SimInTech не обойтись. Так уж сложилось, что есть, что сказать на обозначенные темы и для нее.
1. С++ и ВКПа
Автором только на Хабре написано более двух десятков статьей на тему ВКПа. И если не в каждой, то в большинстве из них подчеркивается, что ее основой является С++. Говоря ВКПа, мы должны подразумевать С++ ровно также, как мы это делаем в контексте упоминания тех же Visual Studio или Qt. Для ВКПа С++ не просто язык, на котором она разработана, но и ее внутренний язык программирования (ЯП). И если, например, MATLAB или SimInTech имеют свои внутренние ЯП, то здесь им является именно С++.
Иметь С++ в двух ипостасях, языка проектирования самой среды и ее внутреннего ЯП, весьма удобно, т.к. не требует специализированного языка программирования среды. Создание последнего - сама по себе задача весьма сложная и трудоемкая. В ВКПа же есть все то, что есть или будет в С++. На текущий момент сюда подключаются также возможности библиотеки Qt и ее средств проектирования (до этого – Visual Studio).
Безусловно, на уровне среды также есть потребность в неком языке программирования. Во многих случаях он позволяет быстро сделать пусть некий набросок решения или реализовать простые функции, не погружаясь в С++. И по существу подобные возможности также можно считать расширением С++. В рамках самого С++ они, во-первых, вылились в создание шаблона описания автоматов в форме таблиц переходов и формирование новой алгоритмической модели.
Активные автоматные объекты качественно расширяют С++. Могу ошибаться, но, думаю, пока в стандартах С++ на эту тему нет и речи. В него внедрили другое: так называемое, многопоточное параллельное программирование, корутины и им подобное. Все это по большей части вызывает только большое сожаление, т.к. добавленные "языковые конструкции" не отвечают теории и только усложняют язык, создавая ему проблемы.
Если говорить о надстройке над С++, то в ВКПа он был расширен диалоговыми средствами реализации простых автоматных алгоритмов. Именно такая ситуация была показана в рамках «забаненной» статьи. Там была создана модель управления нагревателем без привлечения мощи языка С++. Получилось проще, быстрее и под силу тем, кто не знает С++, но должен хоть немного понимать в автоматах. Причем речь идет не о диаграммах Харелла, а о более простых и строгих – классических автоматах.
Покажем разницу двух подходов, приведя реализацию блоков проекта «Нагреватель» из упомянутой выше статьи на С++. Первый листинг (листинг. 1) – автомат управления, второй – простейший переключатель (листинг 2). По технологии проектирования автоматов на С++ требуется внести определенные дополнения в код их реализации, что не так уж сложно. Сюда, во-первых, входит создание необходимых ссылок на переменные среды и указателей на другие процессы, используя стандартный метод автоматного класса - FCreationOfLinksForVariables. Во-вторых, добавить в автомат петлю при начальном состоянии, нагруженную предикатом и действием (см. листинги). В процессе работы только после создания ссылок автомат выполняется переход в те или иные рабочие состояния автомата. Заметим, что эти же действия на уровне диалогов среды (см. применение диалога FAutomaton в [1]) реализуются средой автоматически. Потому-то автомат там и выглядит проще.
Листинг 1. Код блока управления нагревателем
#include "lfsaappl.h"
class FHeatingControl :
public LFsaAppl
{
public:
LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FHeatingControl(nameFsa, pCVarFsaLibrary); }
bool FCreationOfLinksForVariables();
FHeatingControl(string strNam, CVarFsaLibrary *pCVFL);
virtual ~FHeatingControl(void) { }
CVar *pVarStrT; // имя переменной температуры
CVar *pVarStrT_set; // имя переменной уставки температуры
CVar *pVarCoolingDelay; // время охлаждения
CVar *pVarHeatingDelay; // время нагрева
protected:
int x1(); int x2(); int x12();
void y1(); void y2(); void y12();
CVar *pVarT; // температура воды
CVar *pVarT_set; // уставка температуры
};
#include "stdafx.h"
#include "FHeatingControl.h"
static LArc TBL_HeatingControl[] = {
LArc("st","st", "^x12","y12"),
LArc("st","off","x12","y2"),
LArc("on","off","^x2","y2"),
LArc("on","off","^x1","y2"),
LArc("off","on","x1^x2","y1"),
LArc()
};
FHeatingControl::FHeatingControl(string strNam, CVarFsaLibrary *pCVFL):
LFsaAppl(TBL_HeatingControl, strNam, nullptr, pCVFL)
{ }
bool FHeatingControl::FCreationOfLinksForVariables() {
pVarCoolingDelay = CreateLocVar("dCoolingDelay", CLocVar::vtInteger, "cooling time");
pVarHeatingDelay = CreateLocVar("dHeatingDelay", CLocVar::vtInteger, "heating time");
pVarStrT = CreateLocVar("strNameT", CLocVar::vtString, "current temperature");
string str = pVarStrT->strGetDataSrc();
if (str != "") { pVarT = pTAppCore->GetAddressVar(pVarStrT->strGetDataSrc().c_str(), this); }
pVarStrT_set = CreateLocVar("strNameT_set", CLocVar::vtString, "temperature setpoint");
str = pVarStrT_set->strGetDataSrc();
if (str != "") { pVarT_set = pTAppCore->GetAddressVar(pVarStrT_set->strGetDataSrc().c_str(), this); }
return true;
}
// predicates
int FHeatingControl::x1() { return pVarT->GetDataSrc() <= pVarT_set->GetDataSrc(); }
int FHeatingControl::x2() { return FIsActiveParDelay(); }
int FHeatingControl::x12() { return pVarT != nullptr && pVarT_set; }
// action
void FHeatingControl::y1() { FCreateParDelay(pVarCoolingDelay->GetDataSrc()); }
void FHeatingControl::y2() { FCreateParDelay(pVarHeatingDelay->GetDataSrc()); }
void FHeatingControl::y12() { FInit(); }
Листинг 2. Код переключателя
#include "lfsaappl.h"
class FHeatingSwitch :
public LFsaAppl
{
public:
LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FHeatingSwitch(nameFsa, pCVarFsaLibrary); }
bool FCreationOfLinksForVariables();
FHeatingSwitch(string strNam, CVarFsaLibrary *pCVFL);
virtual ~FHeatingSwitch(void) { }
CVar *pVarStrInput;// имя входной переменной
CVar *pVarStrInput1;// имя входной переменной
CVar *pVarStrInput2;// имя входной переменной
CVar *pVarStrOutput;// имя выходной переменной
protected:
int x1(); int x12();
void y1(); void y2(); void y12();
CVar *pVarInput;// адрес входной переменной
CVar *pVarInput1;// адрес входной переменной
CVar *pVarInput2;// адрес входной переменной
CVar *pVarOutput;// адрес выходной переменной
};
#include "stdafx.h"
#include "FHeatingSwith.h"
static LArc TBL_HeatingSwitch[] = {
LArc("st","st","^x12", "y12"),
LArc("st","s1","x12", "--"),
LArc("s1","s1","^x1", "y2"),
LArc("s1","s1","x1", "y1"),
LArc()
};
FHeatingSwitch::FHeatingSwitch(string strNam, CVarFsaLibrary *pCVFL):
LFsaAppl(TBL_HeatingSwitch, strNam, nullptr, pCVFL)
{ }
bool FHeatingSwitch::FCreationOfLinksForVariables() {
pVarStrInput = CreateLocVar("strNameInput", CLocVar::vtString, "input");
string str = pVarStrInput->strGetDataSrc();
if (str != "") { pVarInput = pTAppCore->GetAddressVar(pVarStrInput->strGetDataSrc().c_str(), this); }
pVarStrInput1 = CreateLocVar("strNameInput1", CLocVar::vtString, "input1");
str = pVarStrInput1->strGetDataSrc();
if (str != "") { pVarInput1 = pTAppCore->GetAddressVar(pVarStrInput1->strGetDataSrc().c_str(), this); }
pVarStrInput2 = CreateLocVar("strNameInput2", CLocVar::vtString, "input2");
str = pVarStrInput2->strGetDataSrc();
if (str != "") { pVarInput2 = pTAppCore->GetAddressVar(pVarStrInput2->strGetDataSrc().c_str(), this); }
pVarStrOutput = CreateLocVar("strNameOutput", CLocVar::vtString, "output");
str = pVarStrOutput->strGetDataSrc();
if (str != "") { pVarOutput = pTAppCore->GetAddressVar(pVarStrOutput->strGetDataSrc().c_str(), this); }
return true;
}
// predicates
int FHeatingSwitch::x1() { return int(pVarInput->GetDataSrc()); }
int FHeatingSwitch::x12() { return pVarInput != nullptr && pVarInput1 && pVarInput2 && pVarOutput; }
// action
void FHeatingSwitch::y1() { pVarOutput->SetDataSrc(nullptr, pVarInput1->GetDataSrc()); }
void FHeatingSwitch::y2() { pVarOutput->SetDataSrc(nullptr, pVarInput2->GetDataSrc()); }
void FHeatingSwitch::y12() { FInit(); }
Код на С++ позволяет, создавать автоматы любой сложности (или, по-другому, активные объекты), которые легко превращаются в элементы библиотек. Включенные в библиотеки подобные активные объекты можно многократно использовать в любом проекте среды ВКПа.
2. О параллелизме ВКПа
"Уж сколько раз твердили миру", что сеть автоматов - параллельная модель, в которой каждый автомат - параллельный процесс, синхронизация которых реализуется путем обмена информацией о состояниях (подробнее о параллелизме автоматов см. [3]). В автоматной сети фактически нет проблем, порождаемых типовым подходом к реализации параллелизма.
Если же мы вернемся к проекту нагревателя, то только прикладных, т.е. относящихся к самому проекту, параллельных автоматов пять. Это рассмотренные выше два автомата, еще два автомата управления индикацией и автомат-интегратор (см. также [1]). Необходимо еще учесть динамически порождаемые параллельные процессы задержек. С учетом диалогов управления средой в нашем проекте в общей сумме одномоментно крутится до двадцати параллельных автоматов. И после этого кто-то еще смеет может утверждать, что в проекте нет параллелизма!? Ну, а если нужно знать точное число параллельных процессов, то их отражает стандартный диалог автоматных пространств. Можно о них получить и более детальную информацию, открыв диалог автоматных переменных. Здесь отражается информация о библиотеке и ее элементе, от которого порожден процесс, и его имя, как процесса. Еще один параллельный диалог позволят управлять локальными переменными процесса. Это ли не в целом параллельное решение, заслуживающее соответствующего тега?
Модель параллельных процессов ВКПа - пример совершенно иного подхода к созданию параллельных решений. В ней все проблемы современных параллельных решений - гонки, тупики, проблемы синхронизация и т.п. решаются на уровне модели процессов, где, повторимся, каждый автомат - это математическая модель отдельного процесса, а все они вместе – формальная модель параллельного множества процессов. Они «по щелчку» синхронизируют свою работу, обмениваясь состояниями. Последнее не просто удобно, а это одновременно формально выверенная процедура взаимодействия автоматов, которая, если потребуется, верифицируется операцией параллельной композиции автоматов.
Сетевая автоматная модель - суть ВКПа. Не признавать ее параллелизм - это лишать человека права дышать, рыбе жить в воде, а птице летать. Ведь, не нужно уточнять, что человек дышит, рыба плавает, а птица летает, а потому нет нужды каждый раз долбить и долбить оговаривать, что ВКПа - это про параллелизм. Можно, правда, называть это "авторским взглядом", но это тот взгляд, который отражен во множестве статей автора и не только на Хабре. Нужны ли еще какие-то аргументы, чтобы отрицать параллелизм ВКПа?
На мой взгляд, если какие-то аргументы и понадобятся, то только в ситуациях, которые невозможно создать/описать в рамках алгоритмической параллельной модели среды ВКПа. И если вдруг (!) такое случится, то это будет крушением основ дискретной кибернетики, в которой автоматы составляют базу [4]. Формально, конечно, подобное исключать нельзя. Допускаю даже, что, может быть, это когда-либо и случится. Но это сродни тому, как пока нельзя доказать отсутствие/наличие Бога. Здесь все сходится к тому, что, как минимум, при жизни автора это не произойдет. Хотя, как знать… Может, где-то что-то уже есть и осталось только опубликовать?
3. Автоматы и SimInTech
Перейдем от общих слов к конкретным делам. И тут вспомним про SimInTech, представив свой вариант реализации автоматных процессов на примере его демонстрационного проекта "Нагреватель" [2]. В этот раз блоками приложения будут автоматные параллельные процессы, реализованные по определенному шаблону на внутреннем ЯП среды. Пример такого решения приведен на рис. 1.

В нем созданы аналоги всех блоков исходного решения в SimInTech (см. рис. 2, а подробнее в [2]). Можно было "упаковать" блоки в субмодели, как это сделано в исходной модели (а мы это еще сделаем), но пока все они будут открыты для оценки структуры и связей проекта.

Рассмотрение начнем с задержки, код которой приведен в листинге 3. Она стартует, когда ее вход (bStart) примет значение не равное нулю. Далее задержка измеряется в дискретных шагах проекта. При этом само ее значение задается в микросекундах - вход delay. На выход блока задержки выдается текущее значение числа шагов задержки – выход value и признак ее активности – выход State (0 - не активна, 1 – активна).
Листинг 3. Код блока «Задержка»

Замечание 1. Чтобы задержка отработала верно, необходимо не только установить значение входа, но до ее истечения его сбросить. В противном случае задержка будет запущена повторно.
Листинг 4. Код блока «Управление»

На листинге 4 представлен код контроллера нагревателя (на рис. 1 – блок «Управление»). Здесь сразу же, т.е. на первом переходе, запускается задержка (40 сек). Для этого нужно установить выход out40 и затем, перейдя в состояние 1 (состояние отключения нагревателя), дождавшись инициации задержки, сбросить тот же сигнал out40. Когда интервал задержки истечет, а температура воды будет меньше заданной, переходим в состояние включения нагревателя – 2, выдав при этом сигнал запуска задержки 20 сек – выход out20. Затем, аналогично состоянию 1 сбрасываем сигнал запуска задержки и ожидаем ее завершения или превышения температуры уставки. Дождавшись или того, или другого, переходим в состояния отключения нагревателя. Далее все повторяется.
Замечание 2. Код автомата на листинге 4 демонстрирует использование «теневого состояния» - tmpState. В процессе работы новое текущее состояние автомата записывается сначала в него, чтобы на новом цикле работы модели стать текущим состоянием.

На рис. 3 приведены результаты тестирования модели. Обращают на себя внимание графики работы задержек. Если до 460-й секунды задержки работают строго последовательно, то далее они пересекаются. На работе модели это не сказывается, но, если подходить строго, такого быть не должно. Для устранения замеченного «непорядка» достато��но ввести сигнал сброса/останова работы задержки.
Замечание 3. Графики задержек демонстрируют параллелизм работы блоков в SimInTech.
4. Где собака порылась?
Но все же, в каком блоке скрываются проблемы «автоматов» исходного проекта? Для локализации ошибки провернем «обратный рефакторинг». В этих целях заменим блоки интегратора – «Интегратор» и ключа – «Key» (см. рис. 1) блоком «Модель нагревателя» из исходного проекта. Получим модель, показанную на рис. 4. Тестирование показало, что результаты работы не изменились. Следовательно, проблема скрывается в блоке «контроллер нагревателя» (см. рис. 2). Только в чем же конкретно эта проблема – вопрос к авторам проекта.

5. Создание модели управления индикацией работы нагревателя
Мы создали модель управления и модель нагревателя, но для реализации в полном объеме технического задания необходимо создать еще и управление индикатором. Возьмем за основу модель, предложенную в статье [1]. Но сначала, используя правильные и хорошие качества SimInTech, запрячем наше управление в субмодель. Такое решение показано на рис. 5.

Теперь можно приступать к модели управления индикатором. Она, как и в [1], будет состоять из автоматов – параллельных автоматов (!) - автоматы зеленого и красного цветов. Именно такое решение показано на рис. 6, где кроме самого проекта – рис. 6а, раскрыта структурная модель субмодели «Индикация» - рис. 6б и далее субмодели «Остывание» и «Нагрев» - рис. 6в и рис. 6г соответственно. А уже их блоки, реализованные на ЯП, приведены соответственно на листингах 5 и 6.
И все бы хорошо, если бы не график индикации, приведенный на рис. 7. Сильно смущает однократный всплеск при переключении индикатора с красного на зеленый (после 475-й секунды). Похоже, что в этот момент суммируются (см. оператор суммы на рис. 6б) значения, определяющие текущий цвет индикатора. По замыслу этого быть не должно.


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

Структуру модели «Управление нагревателем» с учетом новых задержек (задержки с сигналом сброса) демонстрирует рис. 9. Заметим, что буферный блок для выхода numstate введен, чтобы транслировать на его выход только состояния 0 и 1, игнорируя остальные состояния блока «Управление» (см. на рис. 8 автомат HeatCntrl и его состояния - 77, 40 и 20).

Листинг 5. Коды блоков управления индикатором.
input bInStateHeat, bInStateLed, bIn5sec;
output nOutColorLed, curState, out5sec;
var tmpState=0;
curState = tmpState;
if curState=0 and bInStateHeat = 0 and bIn5sec = 0 and bInStateLed = 0 then
begin out5sec=1; nOutColorLed = 1; tmpState = 51; end
if curState=51 and bIn5sec > 0 then begin out5sec=0; tmpState = 1; end
if curState=1 and bIn5sec = 0 then begin out5sec = 1; nOutColorLed = 0; tmpState = 50; end
if curState=1 and bInStateHeat = 1 then
begin nOutColorLed = 0; tmpState = 0; end // переход при резком изменении режима
if curState=50 and bIn5sec > 0 then begin out5sec=0; tmpState = 0; end
input bInStateHeat, bInStateLed, bIn5sec;
output nOutColorLed, curState, out5sec;
var tmpState=0;
curState = tmpState;
if curState=0 and bInStateHeat = 1 and bIn5sec = 0 and bInStateLed = 0 then
begin out5sec=1; nOutColorLed = 2; tmpState = 51; end
if curState=51 and bIn5sec > 0 then begin out5sec=0; tmpState = 1; end
if curState=1 and bIn5sec = 0 then begin out5sec = 1; nOutColorLed = 0; tmpState = 50; end
if curState=1 and bInStateHeat = 0 then
begin nOutColorLed = 0; tmpState = 0; end // переход при резком изменении режима
if curState=50 and bIn5sec > 0 then begin out5sec=0; nOutColorLed = 0; tmpState = 0; end
А теперь поговорим начистоту. Среда SimInTech – программная реализация, как представляется, гениальной идеи, однако, требующее существенной проверки и доработки. Это пояснит почти любой программист, хорошо знакомый с обычными универсальными языками программирования и средами их разработки - IDE. С гениальностью – сложнее. Лично меня давно «сразила» среда МВТУ, которая единственная на тот момент ввела RS-триггер в режим генерации (правда, ненадолго). А поскольку она – предтеча SimInTech, то восхищение первой естественным образом распространилось и на вторую. Тем более, что в SimInTech есть и другие интересные и интересующие меня черты.
В контексте обсуждения понятия «гениальность» хочется возразить тем, кто считает Россию лучшей страной в мире. Она – не лучшая. Но Россия в чем-то гениальная страна, выживающая в жестком мире рыночной экономики за счет своих гениальных качеств, которые пока еще работают. Помнится, когда-то меня очень впечатлил мультик «Самый, самый, самый, самый». В нем львенок стремится стать самым сильным, самым красивым, самым отважным, самым умным или, короче, самым, самым, самым. Хотя бы потому, что по определению он «Царь зверей». В конечном итоге он им и становится (а куда тут денешься – по праву рождения), но до него доходит, что каким бы сильным он не был – найдется сильнее, каким бы ты красивым не стал – найдется красивее, найдутся отважнее, быстрее, умнее и т.д. и т.п. Бывает и такое – умный царь! И лев - уже взрослый лев - понимает, что главное счастье - найти того, в глазах кого ты будешь самым-самым-самым-самым. И он находит – ее (правда, что уж там скрывать, она его). Ту, для которой он самый-самый. Счастливый конец. Конец, ради которого стоит жить и творить льву и, наверное, любому другому… Так что – смотрите мультики. Лучше старые, советские мультики... А не этот, как его, - «Король Лев».
Россия – далеко не лучшая страна мира. Это факт – статистический. Но есть много, очень много, даже миллионы людей, для которых она самая-самая. И это главное для России, да и, надо понимать, для любой другой страны мира. Смысл жизни: не стремиться быть самым-самым и для этого лезть из кожи. Быть «халифом на час» - не тот путь, который приведет к счастливой жизни. Ну и, конечно, Россию.
Среда SimInTech – явно не лучшая среди себе подобных. Но в ней есть нечто, что на текущий момент делает ее для меня единственной и неповторимой. Познакомившись с SimInTech лишь недавно (с конца прошлого – 2022 года) и, что там скрывать, зная ее достаточно поверхностно, это, тем не менее, позволило в ней реализовать важные для меня «автоматные идеи». Со средой МАТЛАБ такой «любви» как-то не случилось, хотя знаком с ней достаточно давно. Почему так – объяснять, наверное, долго и нудно. Но факт остается фактом – не случилось и все тут. Хотя, как теперь понимаю, в ней можно тоже провернуть нечто подобное. Но все равно SimInTech – первая. Как первая любовь…
Автоматы в SimInTech – совсем не те автоматы, которые я могу признать и даже назвать автоматами. Безусловно, есть и, вероятно, будут те, кто со мной не согласится, и для них они - самые-самые конечные автоматы (как и диаграммы Харелла). Но только, похоже, они теорию автоматов давно не пересматривали. Или они ее просто не признают или уж, что совсем крайний случай, о ней они просто не знают…
Но, как ни крути, в теорию автоматов я все же влюбился раньше. Но ни что не мешает ее чертами наградить и SimInTech. Как это сделать в первом приближении мы показали выше. Может это сложнее того, чем можно было бы сделать, если подумать, но, зато, много надежнее. И уж точно лучше, чем ничего.
Литература
1. Автоматное программирование в SimInTech и ВКПа. [Электронный ресурс], Режим доступа: https://habr.com/ru/post/709358/ свободный. Яз. рус. (дата обращения 28.02.2023).
2. Конечные автоматы в среде динамического моделирования SimInTech. [Электронный ресурс], Режим доступа: https://habr.com/ru/post/307090/ свободный. Яз. рус. (дата обращения 28.02.2023).
3. Модель параллельных вычислений. [Электронный ресурс], Режим доступа: https://habr.com/ru/post/486622/ свободный. Яз. рус. (дата обращения 28.02.2023).
4. Глушков В.М. Введение в кибернетику. Изд-во АН Украинской ССР. К.: 1964. - 324с.
5. Автоматная модель управления программ. [Электронный ресурс], Режим доступа: https://habr.com/ru/post/484588/ свободный. Яз. рус. (дата обращения 28.02.2023).
