Но почему, почему, почему
Был светофор зеленый?
А потому, потому, потому,
Что был он в жизнь влюбленный.

Введение
Светофоры, возможно, не самый, но достаточно популярный пример. Они бывают разные: может быть один или несколько светофоров, автомобильные светофоры и светофоры для пешеходов, для сельских и городских дорог и т.д. и т.п. Даже светофоры Дейкстры в тему. А тут недалеко и до многопоточности, параллелизма и даже, не к ночи будет помянуто, к конкурентности.
Один из первых примеров, на которых автором оттачивалась автоматная технология параллельного программирования, был светофор С. Ангера из книги «Асинхронные последовательностные схемы». Это хороший пример, к которому, возможно, мы еще вернемся. Кстати, на Хабре уже есть статья про светофоры [1]. Светофоров, как говорится, много не бывает.
Задачи типа светофоров привлекают проблемами, которые они отражают, но еще больше тем, как они (проблемы) могут быть разрешены в рамках автоматной модели. И, казалось бы, проект светофоров на базе библиотеки «Конечные автоматы» с сайта Engee в этом плане далеко не самый сложный и примером серьезных проблем не является (см [2]).
Но это я так думал, когда приступал к реализации. И, как это часто бывает, не все, что выглядит просто, таким же простым является на самом деле.
Светофоры в Qt
Решение продолжить тему светофоров Engee пришло из ощущения, что даже такой простой пример можно сделать еще проще, продемонстрировав возможности технологии автоматного программирования (АП) в сравнении с моделью автоматов в Engee. Словом, почему бы и нет.
Работа над проектом начинается с создания класса светофора. Воспользуемся для этого возможностями ООП и опытом предыдущей статьи[3]. Возьмем за базу уже существующий класс светофора FTrafficLight, а новый сделаем таким:Листинг
Листинг 1. Код нового светофра
#include "FTrafficLight.h"class FLightCntr: public FTrafficLight{public: LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FLightCntr(nameFsa, pCVarFsaLibrary); } bool FCreationOfLinksForVariables(); FLightCntr(string strNam, CVarFsaLibrary *pCVFL);};#include "FLightCntr.h"LArc TBL_LightCntr[] = { LArc("st","R","x1","--"), LArc("st","Y","x2","--"), LArc("st","G","x3","--"), LArc("R","RY","--","y2y3y5y7"),//R=1, Y=0, G=0, 30sec LArc("RY","G","--","y4y8"), //R=1, Y=1, G=1, 5 sec LArc("G","G0","--","y1y3y6y9"),//R=0, Y=0, G=1, 10 sec LArc("G0","G1","--","y5y10"),//R=0, Y=0, G=0, 1 sec LArc("G1","G01","--","y6y10"),//R=0, Y=0, G=1, 1 sec LArc("G01","G10","--","y5y10"),//R=0, Y=0, G=0, 1 sec LArc("G10","G02","--","y6y10"),//R=0, Y=0, G=1, 1 sec LArc("G02","Y","--","y5y10"),//R=0, Y=0, G=0, 1 sec LArc("Y","R","--","y1y4y5y8"),//R=0, Y=1, G=0, 5 sec LArc()};FLightCntr::FLightCntr(string strNam, CVarFsaLibrary *pCVFL): FTrafficLight(strNam, pCVFL, TBL_LightCntr){ }bool FLightCntr::FCreationOfLinksForVariables() { FTrafficLight::FCreationOfLinksForVariables(); return true;}
Здесь таблица переходов (ТП) заимствована из базового класса. Перегружен и его метод FCreationOfLinksForVariables(). Именно здесь будет создан параллельный процесс по отношению к текущему классу.
Но сначала внесем изменения в базовый класс. Введем в его конструктор указатель на ТП, что позволит порождать класс с заданным поведением. Подключим созданный класс к библиотеке, названной, естественно, Engee, в которую мы и дальше будем помещать примеры для одноименной среды.
Использование ранее созданного проекта значительно упростит настройку графической части нового проекта, который в этом случае по структуре и поведению будет копией старого. Это подтверждает и рис. 1.
Удалим любой из светофоров. Пусть это будет автоматная переменная 2-го светофора — TrafficLight_2 (см. рис. 2). Вид диаграммы сигналов после этого станет таким, как на рис. 3:
Графики сигналов удаленного светофора пропали, но его настройки сохранились. Вставим новый светофор с именем только что удаленного. После перезапуска проекта получим тот же вид графиков просто потому, что вставленный светофор копирует поведение светофора удаленного.
Итак, на текущий момент отличие проектов будет сводиться к списку автоматных переменных, где для переменной с именем TrafficLight_2 выбран блок — FLightCntrl библиотеки Engee (см. рис. 4).
Заготовка проекта создана и можно приступать к изменению кода светофора, наблюдая по виду графиков за его поведением.
Класс светофора со счетчиком
Новый класс будет включать класс циклического счетчика, который будет параллельно основному процессу считать такты автоматного пространства. Цикличность будет заключаться в возобновлении счета при достижении какого-то значения. В нашем случае это будет максимальное число тактов диаграммы сигналов (см. рис. 2 в [4]).
В Engee в аналогичной ситуации мы говорили бы о создании параллельного состояния. Здесь же мы говорим о параллельных процессах, да и автоматных пространств с разным дискретным временем в Engee нет. Только в ВКПа процесс-счетчик можно поместить в автоматное пространство, которому задать нужное дискретное время. При этом сама модель светофора может работать в более быстром пространстве. Лишь бы оно не было медленнее.
Листинг 3. Код счетчика
class FCounter : public LFsaAppl{public: FCounter(int n); virtual ~FCounter(void); FLightCntr *pFLightCntr{nullptr}; int nCounter{0};protected: int x1(); void y1(); void y2(); int nMax{0};friend class FLightCntr;};static LArc TBL_Counter[] = { LArc("s1","s1","x1", "y1"), LArc("s1","s1","^x1", "y2"), LArc()};FCounter::FCounter(int n): LFsaAppl(TBL_Counter, "FCounter"){ nCounter = -1; nMax = n;}FCounter::~FCounter(void) { }int FCounter::x1() { return nCounter < nMax; }void FCounter::y1() { nCounter++; if (pFLightCntr) pFLightCntr->pVarCntr->SetDataSrc(nullptr, nCounter);}void FCounter::y2() { nCounter = 0; if (pFLightCntr) pFLightCntr->pVarCntr->SetDataSrc(nullptr, nCounter);}
Модель счетчика содержит одно состояние. По достижении максимального значения счетчик сбрасывается действием y2. После этого счет возобновляется.
Код создания параллельного процесса-счетчика показан на листинге 4. Здесь же создаются локальные переменные – максимальное значение счетчика, текущее значение счетчика и имя автоматного пространства для загрузки счетчика. Также в зависимости от заданного начального цвета светофора (локальная переменная nInitColor базового класса) определяется номер такта (см. диаграмму на рис. 2 в [2]) с которого начнет работу светофор.
Листинг 4. Код создания параллельного процесса счетчика.
bool FLightCntr::FCreationOfLinksForVariables() { FTrafficLight::FCreationOfLinksForVariables(); pVarMaxCntr = CreateLocVar("max_cntr", CLocVar::vtInteger, "", true, "54"); pVarCntr = CreateLocVar("сurrent_cntr", CLocVar::vtInteger, ""); pVarStrNameNet = CreateLocVar("strNameNet", CLocVar::vtString, ""); string strNet = pVarStrNameNet->strGetDataSrc(); nMaxCntr = pVarMaxCntr->GetDataSrc(); if (pFCounter==nullptr) { if (strNet == "") { pNet = GetPointerToNet(); } else { pNet = GetPointerToNet(strNet); } if (strNet == "") { pNet = GetPointerToNet(); } else { pNet = GetPointerToNet(strNet); } if (pNet) { pFCounter = new FCounter(nMaxCntr); pFCounter->pFLightCntr = this; pFCounter->FLoad(pNet, "Counter", 1); pNet->go_task(); pFCounter->FStop(); } if (pFCounter) { if (x1()) { pFCounter->nCounter = 0; } if (x2()) { pFCounter->nCounter = 50; } if (x3()) { pFCounter->nCounter = 35; } } pFCounter->FStop(); } return true;}
На листинге 5 представлена таблица переходов светофора, его предикаты и действия.
Листинг 5. Код класса светофора
LArc TBL_LightCntr[] = { LArc("tt","tt","^x12","y12"), LArc("tt","st","x12", "--"), LArc("st","st","x4", "y14"), LArc()};…int FLightCntr::x4() { return pFCounter->nCounter != nSavCntr; }int FLightCntr::x12() { return pNet != nullptr && pFCounter; }void FLightCntr::y14() { if (pFCounter->nCounter == 0) { y17(); y2(); y3(); y5(); y16(); } if (pFCounter->nCounter == 30) { y17(); y4(); } if (pFCounter->nCounter == 35) { y17(); y1(); y3(); y6(); y15(); } if (pFCounter->nCounter == 45) { y17(); y5(); } if (pFCounter->nCounter == 46) { y17(); y6(); } if (pFCounter->nCounter == 47) { y17(); y5(); } if (pFCounter->nCounter == 48) { y17(); y6(); } if (pFCounter->nCounter == 49) { y17(); y5(); } if (pFCounter->nCounter == 50) { y17(); y4(); }}void FLightCntr::y17() { nSavCntr = pFCounter->nCounter; }
В начальном состоянии «tt» проверяется указатель на автоматное пространство. Если оно найдено, то выполняется переход в состояние «st», где предикат x4 анализирует счетчик. Когда значение счетчика изменяется, запускается действие y14, которое устанавливает цвет светофора и запоминает текущее значение счетчика. Последнее необходимо, чтобы исключить повторные срабатывания, когда модель светофора работает в более быстром автоматном пространстве.
О модели светофора
Новая модель светофора стала не только проще, но имеет качества, которые отсутствуют у базовой модели. В рамках ее:
1) исключена работа с таймерами;
2) модель стала точнее;
3) резко снизились требования к длительности дискретного времени процессов;
4) изменяя дискретное время, можно изменять скорость работы светофоров.
Но есть и проблема. По отдельности светофоры точно реализуют временную диаграмму, но возможен разнобой в работе, что отразят графики, которые будут в этом случае сдвинуты относительно друг друга.
Для решения проблемы процессы-счетчики сразу же после создания останавливаются (см. на листинге 4 метод FStop()). Затем они одновременно запускаются созданным для этого параллельным процессом. Он имеет диалог, в котором указывается задержка запуска процессов и имеется кнопка ручного запуска. В среде процесс можно настроить, чтобы он запускал процессы автоматически.
Упрощаем модель
Но зачем счетчики, если модели могут сами считать такты? Код такого светофора приведен на листинге 6. Наращивание/сброс счетчика тактов происходит в действии y14.
Листинг 6. Код автомобильного светофора-счетчика.
#include "FTrafficLight.h"class FLightEasy: public FTrafficLight{public: LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FLightEasy(nameFsa, pCVarFsaLibrary); } bool FCreationOfLinksForVariables(); FLightEasy(string strNam, CVarFsaLibrary *pCVFL); CVar *pVarMaxCntr; CVar *pVarCntr;protected: void y14(); int nCounter{0};};#include "stdafx.h"#include "FLightEasy.h"LArc TBL_LightEasy[] = { LArc("st","st","--","y14"), LArc()};FLightEasy::FLightEasy(string strNam, CVarFsaLibrary *pCVFL): FTrafficLight(strNam, pCVFL, TBL_LightEasy){}bool FLightEasy::FCreationOfLinksForVariables() { FTrafficLight::FCreationOfLinksForVariables(); pVarMaxCntr = CreateLocVar("max_cntr", CLocVar::vtInteger, "", true, "55"); pVarCntr = CreateLocVar("сurrent_cntr", CLocVar::vtInteger, ""); nCounter = 0; if (x1()) { nCounter = 0; } if (x2()) { nCounter = 50; } if (x3()) { nCounter = 35; } return true;}void FLightEasy::y14() { pVarCntr->SetDataSrc(nullptr, nCounter); if (nCounter == 0) { y2(); y3(); y5(); y16(); } else if (nCounter == 30) { y4(); } else if (nCounter == 35) { y1(); y3(); y6(); y15(); } else if (nCounter == 45) { y5(); } else if (nCounter == 46) { y6(); } else if (nCounter == 47) { y5(); } else if (nCounter == 48) { y6(); } else if (nCounter == 49) { y5(); } else if (nCounter == 50) { y4(); } nCounter++; if (nCounter == pVarMaxCntr->GetDataSrc()) nCounter=0;}
Код пешеходного светофора, который выглядит еще проще, приведен на листинге 7.
Листинг 7. Код пешеходного светофора.
class FCrossEasy: public FCrosswalk{public: LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FCrossEasy(nameFsa, pCVarFsaLibrary); } bool FCreationOfLinksForVariables(); FCrossEasy(string strNam, CVarFsaLibrary *pCVFL); CVar *pVarMaxCntr;protected: void y14(); int nCounter{0};};LArc TBL_CrossEasy[] = { LArc("st","st","--","y14"), LArc()};FCrossEasy::FCrossEasy(string strNam, CVarFsaLibrary *pCVFL): FCrosswalk(strNam, pCVFL, TBL_CrossEasy){ }bool FCrossEasy::FCreationOfLinksForVariables() { FCrosswalk::FCreationOfLinksForVariables(); pVarMaxCntr = CreateLocVar("max_cntr", CLocVar::vtInteger, "", true, "55"); pVarCntr = CreateLocVar("сurrent_cntr", CLocVar::vtInteger, ""); nCounter = 0; return true;}void FCrossEasy::y14() { pVarCntr->SetDataSrc(nullptr, nCounter); if (nCounter == 0) { y2(); y3(); y8(); } else if (nCounter == 20) { y1(); y4(); } else if (nCounter == 30) { y2(); y3(); } nCounter++; if (nCounter == pVarMaxCntr->GetDataSrc()) nCounter=0;}
Теперь все светофоры работают в автоматном пространстве с секундным дискретным временем. В окончательную версию проекта добавлен процесс, генерирующий коды состояния светофоров (см. строку «код» на временной диаграмме на рис. 2 в [3]). Совмещенные графики текущего значения счетчика тактов и строки «код» временной диаграммы показаны на рис. 5. Она имеет такой же вид, как и на странице проекта сайта Engee.
ESP32
Следующий важный шаг включает создание упрощенной версии ядра ВКПа в Qt и перенос ее в микроконтроллер (МК) типа ESP32. Так появляется возможность создавать и отлаживать проекты в Qt, а затем переносить их на МК. Соответственно на МК будет альтернативная потокам параллельная технология исполнения программ в жестком реальном времени.
Для оценки быстродействия автоматного ядра среды ВКПа создадим на ESP32 три проекта. Первый проект – это упрощенный вариант исходного решения в ВКПа. Второй – реализация моделей по типу SWITCH-технологии. И третий вариант, который мы назовем «типичным».
Во втором варианте не будет ядра интерпретации автоматов, без которого он будет явно более быстрым решением. С ним и будет сравниваться эффективность ядра среды ВКПа. В третьем варианте не будет совсем автоматов. Это выбор тех, кто в силу принципиальной позиции или каких-то иных причин их игнорирует. Среди программистов таких пока еще большинство.
Мини-ВКПа в Qt и VS Code
Для первого варианта в Qt Creator создаем проект типа «Приложение Qt Widgets», в который переносим прикладные классы исходного проекта в ВКПа. Отсутствие dll-библиотек требует также включение кода автоматного ядра интерпретации автоматов. Кому-то подобные проекты даже нужны, а для нас это лишь переходная форма к проектам на ESP32. Здесь не будет окружения ВКПа, которое, если потребуется, придется создавать «ручками». Например, графику.
Для удобства в рамках основного окна приложения сформируем диалог, который будет отражать функционирование светофоров и параметры их работы: кодирование цвета светофоров, текущее значение счетчика тактов, число циклов работы светофоров и любую другую информацию. Можно создать и отдельный диалог для светофора. Вид подобного интерфейса показан на рис. 6.
Перенос проекта из ВКПа в проект среды ESP32 на Qt (назовем его пока так) занимает буквально минуты, т.к. код был заимствован из рабочего проекта. Перенос проекта в VS Code также выполняется достаточно просто, т.к. отработан на предыдущих проектах.
SWITH-технология
В процессе работы захотелось превратить светофоры в тест оценки эффективности работы автоматов. Микроконтроллер удобен тем, что здесь нет прослойки в форме операционной системы, которая есть на ПК, а SWITH-вариант исключает еще и интерпретацию автоматов.
Код пешеходного светофора в формате SWITH-решения показан на листинге 8 (вырезка из среды VS Code):
Листинг 8. Код пешеходного светофора.
#include "Config.h"#include "FCrossEasy.h"FCrossEasy::FCrossEasy(String strNam): FCrosswalk(strNam){}void FCrossEasy::run() { y14(); }void FCrossEasy::y14() { pVarCntr = nCounter; if (nCounter == 0) { y2(); y3(); y8(); } else if (nCounter == 20) { y1(); y4(); } else if (nCounter == 30) { y2(); y3(); } nCounter++; if (nCounter == pVarMaxCntr) nCounter=0;}
Коды листингов 7 и 8 по сути мало чем отличаются друг от друга. Все сводится к реализации таблиц переходов и создания некоего окружения в случае ВКПа. Например, если на листинге 7 ТП в явном виде, то на листинге 8 это метод run(). Именно данный метод реализует логику работы автомата на базе оператора типа SWITH. В нашем светофоре в нем необходимости нет, т.к. работа модели сводится к циклическому запуску действия y14().
Эксперименты показывают, что в асинхронном режиме ядро ВКПа на реализацию диаграммы светофоров тратит примерно 9-11 мсек, тогда как в SWITH на это уходит 2-3 мсек. Замедление примерно в пять раз. Иногда это критично.
Замечание. Тестирование убеждает, что, например, в режиме интерпретации автоматы на ПК работают совсем не быстрее, а по факту даже медленнее (?), чем на ESP32. Неужели столь велико влияние «прослойки»? Код-то на уровне С++, ведь, один и тот же.
Типовое решение светофоров
В качестве типичного программиста был выбран ИИ. Ему, т.е. DeepSeek, было выдано техническое задание в следующей формулировке (см. также сайт Engee):
«Разработать модель управления перекрестком, состоящим из двух автомобильных и одного пешеходного потоков, которые управляются двумя трёхсекционными и одним двухсекционным светофорами соответственно. Алгоритм переключения секций будет определяться согласно временной диаграмме.
Временная диаграмма работы светофоров
Три потока — два автомобильных и один пешеходный управляются асинхронно. При этом разрешение на движение (зелёный свет) одновременно формируется только для одного потока и длится 10 секунд. После окончания разрешения на движение автомобильного потока включается мигание зелёной секцией. Переход светофора из разрешающего сигнала в запрещающий (красный свет) сопровождается включением жёлтой секции на 5 секунд, обратный переход — одновременным включением красной и жёлтой секций на 5 секунд. Проект разработать для ESP32 на языке С++ в среде VS Code».
DeepSeek, видимо, имел уже готовое решение, т.к. тут же выдал полторы тысячи строк кода, из которых за реализацию диаграммы отвечали примерно триста, а остальные — за подключение к WiFi и отображение web-странички. Ее внешний вид и саму работу светофоров демонстрирует следующая гифка:
Сразу полезли ошибки: мигание светофоров не соответствуют заданию, пешеходный светофор не должен мигать и т.п. Не добившись от DeepSeek правильного решения, пришлось предложить реализацию строго по тактам в соответствии с диаграммой.
Так совместными усилиями было создано решение от «типичного программиста», скорость работы которого фактически совпала со SWITCH-решением.
Выводы
Рассмотренная задача о светофорах отражает процессы, которые не видны на других примерах. Созданное решение показывает, как свойства синхронной среды можно использовать в параллельном программировании, отказавшись от стандартных механизмов синхронизации многопоточности.
«Типичное решение» кажется и простым и эффективным. Но это поверхностный взгляд. Кустарное производство тоже часто более эффективно. Но от этого оно не перестает быть таковым и качественно не в лучшую сторону отличается от промышленного подхода. Так и автоматы в программировании. Они превращают «кустарное программирование» в промышленное и высокотехнологичное.
Ну, а когда целый час горит зеленый свет, то это только означает, что светофор сломался. Сам собой включаться и гореть одним цветом столь длительное время он не должен. Правда, в жизни всякое бывает. Особенно, когда … доминирует влюбленность или проектирование проведено на кустарном уровне. Но все это далеко за пределами промышленного автоматного программирования.
Код ядра ВКПа и рассмотренных примеров размещен на GitHab и доступен по ссылке: https://github.com/lvs628/VCPa.git. Реальный проект с автоматами, реализованными в формате SWITCH, доступен по ссылке: https://github.com/woronin/HotbedAgroControl.
Литература
1. Держись, Маша! Ты, ведь, наша! Продолжение разбора книги «Цифровая схемотехника и архитектура компьютера», https://habr.com/ru/articles/780162/
2. Библиотека «Конечные автоматы». https://start.engee.com/state_machines
3. Браслет для Бони. https://habr.com/ru/articles/1030712/
4. Бониана: приложение к браслету. https://habr.com/ru/articles/1032508/
ссылка на оригинал статьи https://habr.com/ru/articles/1044514/