Аннотация

В статье рассматривается библиотека на C++, которая предназначена для реализации технологии параллельного автоматного программирования (АП), отвечающей концепции среды ВКПа (подробнее о ней см. [1]). Для полного понимания материала рекомендуется ознакомиться с основами теории АП, представленной в статьях [2, 3, 4], Взаимосвязь машины Тьюринга с конечными автоматами (КА) подробно рассмотрена в [5]. Вопросы применения корутин в контексте автоматного программирования анализируются в статьях [6-9]. Но в минимальном варианте достаточно даже общего представления о модели конечного автомата и принципах объектного программирования.

Цели работы: 

  1. Ознакомление разработчиков с универсальной технологией проектирования программного обеспечения. 

  2. Реализация технологии АП в виде библиотеки на C++ для микроконтроллеров ESP32, что позволяет применять передовые методы разработки, характерные для крупных платформ, в ресурсоограниченных средах. 

Ключевые преимущества: 

Объединение модульного, объектно-ориентированного и параллельного программирования в единую концепцию на основе строгой математической теории конечных автоматов кардинально меняет подход к проектированию, отладке и документированию ПО. Это устраняет зависимость от интуитивных и зачастую кустарных методик, характерных для традиционного программирования. 

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

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

Можно ли сделать корутинную модель программирования удобнее, проще и, главное, истинно параллельной?  Это может быть ситуация, когда алгоритм содержит обязательные компоненты, которые можно считать точками прерывания. Как минимум, одна такая алгоритмическая модель существует.  

Но это не широко известная модель блок-схем (БС). С другой стороны, можно представить множество БС, где  каждая представляет корутину, а прерывания выполняются после каждой команды. Реализовать подобное не сложно, но будут большие потери, связанные с переключением процессов. Оптимизация заключается в уменьшении числа прерываний. Так поступают, настраивая режим многопоточности. Но поскольку при этом прерывания совершаются хаотично и не тогда, когда это нужно, то возникает клубок проблем.

Расставляя точки прерывания, программист в чем-то микширует определенные проблемы. Может, в этом и есть один из смыслов корутин? Но проблем в любом случае не становится меньше даже с ними. А поскольку процесс разрешения проблем параллелизма интуитивен, то это не способствует созданию качественного и надежного программного кода.

Конечно-автоматные корутины и процессы

Однако, как мы уже сказали, есть модель, которая перекрывает возможности корутин и потоков. Это сетевая конечно-автоматная модель. Сама модель конечного автомата (КА) программистам известна давно. На ней основан, например, метод введения переменной состояния, предложенный Ашкрофтом и Манной еще в 70-х годах прошлого века[12]. Заметим, что он входит в число наиболее эффективных методов проектирования программ.

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

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

Например, на любом языке программирования довольно просто запрограммировать фразу типа: «если случилось что-то, то сделать то-то». Выражению таких мыслей служит оператор IF-ELSE. Подобное мышление поощряют и другие управляющие операторы типа FOR, WHILE, SWITH и т.п. Но в таком разнообразии есть свой минус. Особенно, если вспомнить, что в категорию управляющих операторов попадает злополучный оператор GO TO.

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

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

Сопрограммы

Термин «корутины» ввел в оборот Мелвин Конвей в 1958 г. в статье «A Multiprocessor System Design». Первая реализация появилась на языке COBOL спустя пять лет — в 1963 г. В целом же активная жизнь корутин связана с 1960-1970-ми годами и языками Simula и Modula-2.  Таким о бразом, процесс их использования имеет давнюю историю. Сейчас мы присутствуем при их в своем роде ренессансе.

Итак, краткое введение в тему. Что же собой представляют корутины, которые советские инженеры называли сопрограммами? На рис. 1. приведена картинка из книжки 80-х годов - Зелковиц М., Шоу А., Геннон Дж. «Принципы разработки программного обеспечения» [14]. Она более чем наглядно поясняет работу сопрограмм.

Рис.1. Сопрограммы
Рис.1. Сопрограммы
Рис.1. Сопрограммы

А если мы откроем «Толковый словарь по вычислительным системам» (под ред. Иллингоута и др.), переведенный на русский язык в 90-х годах, то узнаем, что «сопрограммы, как правило, не встречаются в высокоуровневых языках» и «представляют особый интерес, как средство моделирования параллельной работы в последовательных машинах» [15]. Это отражение ситуации 90-х годов. Если бы авторы словаря только могли предположить, что ожидает высокоуровневые языки в будущем? Но с моделированием они явно угадали.

Сопрограммы привлекают детским примитивизмом, но напрягают ручной расстановкой точек прерывания. А можно ли это как‑то исправить? Ведь, сама идея сопрограмм удачна для реализации параллелизма. Пусть даже в режиме имитации. А когда‑то, ведь, и выхода другого не было. Когда о многоядерности еще не знали, а потоки только‑только зарождались! т. е. крутись, как хочешь. Вот и докрутились! До корутин.

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

В идеале язык описания автоматов должен соответствовать формальному определению модели. Он должен быть эффективно реализуемым и иметь иерархическую (вложенную) модель для создания аналога программных функций. Важно, что в теории КА с подобной иерархией есть проблемы, но в нашем случае они решены (см., например, [2]). Отдельного рассмотрения заслуживает также проблема инерционности программных процессов, решенная в рамках автоматной модели, но никак не представленная и более того игнорируемая (!) современной концепцией программирования.

Кандидат, обладающий требуемыми качествами, на момент создания сопрограмм имелся. Это таблицы решений (TP). В книге Э.Хамби «Программирование таблиц решений» они описаны весьма подробно [13]. Изучив их, можно сделать заключение, что таблицы решений — это автоматы, но без состояний. Или, другими словами, любая ТР — это автомат с одним состоянием, по которое просто забыли.

Таким образом, налицо связь между ТР и табличной формой представления автоматных процессов. Попутно отметим один замечательный факт: одна табличная форма КА заменяет все разнообразие управляющих операторов любого языка программирования. Таблицы решений допускают и эффективную аппаратную реализацию (например, использование ассоциативной памяти). У какой модели вы еще такое видели?!

Внедряя автоматы, удобно изменить представление о структуре программы, разделив ее монолитную конструкцию на три составные части — память, управление и операторы. А операторы разделить на предикаты и действия, где первые — это функции, предназначенные делать любой анализ и возвращающие булевское значение. Вторые — выпоняют конкретную работу, не возвращая значений. Кстати, так или примерно так поступают при реализации ТР.

Довести идею корутин на базе автоматов до рабочего состояние поможет объектно‑ориентированное программирование (ООП). В этом случае каждый поток представляется [автоматным] объектом, у которого предикаты и действия — суть его методы, а свойства объекта — память автоматной программы.

Остается только внедрить в тело такого объекта таблицу переходов (ТП) автомата, что совсем уж дело техники. За это будет отвечать базовый автоматный объект (ниже это LFsaAppl), от которого будут порождены уже прикладные объекты. Такая таблица будет заданием, которое управление должно реализовать, если это делается аппаратно, а если же программно, — интерпретировать. Речь здесь идет об упомянутом выше ядре.

Есть от автоматного программирования польза и объектам: у них появляется поведение. Подобные объекты называют акторами. Только теперь в основе их поведения лежит строгая математическая модель — конечный автомат. Такие объекты мы будем назвать программными автоматами, автоматными процессами или, когда это понятно из контекста, просто автоматами. Понимая при этом, что такой объект — это программа, представленная активным программным объектом, который имеет управление в форме КА.

Таким образом, можно вполне представить, как должно измениться мышление программиста, использующего автоматы. В рамках технологии АП он оперирует строгими математическими понятиями, когда отдельный процесс и алгоритм его работы — это КА, а их множество — сеть взаимодействующих автоматов, работающих в едином времени. Из этого следует, что применение теории конечных автоматов (ТКА), которая востребована в практике проектировании цифровых схем, принесет огромную пользу и программированию. Особенно параллельному.

Процесс внедрения какой‑либо концепции в иную для нее область представляет, порой, долгую, а для кого‑то даже мучительную процедуру. Особенно, когда нужно не просто переучиться, а изменить свое мышление. Подобная «ломка» происходит, например, при переходе от обычного программирования к объектному. И пусть это не самый простой и приятный процесс, но он стоит того. Сами же детали подобной процедуры мы уточним по ходу освоения технологии автоматного программирования (АП) на конкретных примерах.

Переходим к практической стороне реализации представленных выше идей.

Автоматные корутины на микроконтроллере

Чем привлекает микроконтроллер? Своей доступностью, а в последнее время и возможностями. С появлением серии ESP32 у микроконтроллеров появился полноценный С++ (даже с библиотекой STL). Стало легко заимствовать созданный для «больших» ПК код. На переносе ядра среды ВКПа все это успешно было проверено.

В результате была создана библиотека для микроконтроллера ESP32. Она позволяет без проблем переносить автоматы, созданные ранее на С++ для «больших ПК». Более того, можно саму разработку и отладку вести на ПК в тех же средах типа Qt Creator и/или Microsoft Visual Studio, где уже есть реализация ВКПа (подробнее см. [1]). Это многократно эффективнее, чем программирование в среде для микроконтроллера (в том же редакторе VS Code). При этом финишную доводку кода следует все же доверять микроконтроллеру.

Итак, предположим, есть датчики, регистрирующие три уровня жидкости, подключенные к входам (пинам) микроконтроллера с номерами 18, 19, 21. Процедуру чтения входов и отображения их значений отражает листинг 1. На листинге 2 показана установка режима входов микроконтроллера — в функции setup() и организация периодического опрос датчика — в функции loop(). Функция setup() выполняется только один раз при запуске микроконтроллера, а потом постоянно «крутится» функция loop(),которая в нашем случае содержит периодический опрос датчиков (один раз в секунду).Это пример типичного проектирования программ для микроконтроллера.

Листинг 1. Опрос и отображения состояния входов датчиков уровня жидкости
#include <Arduino.h>
// Номера входных пинов датчика
int gpioLevel1{18}; int gpioLevel2{19}; int gpioLevel3{21};
// текущее состояние входов
bool level1{0x0}; bool level2{0x0}; bool level3{0x0}; 
float fLevel{0}; float fSavLevel{-1};
// Отображение состояния датчика уровня
void LevelView()
{
  level1 = digitalRead(gpioLevel1);
  level2 = digitalRead(gpioLevel2);
  level3 = digitalRead(gpioLevel3);
  if (!level1&&level2&&level3) fLevel=30;
  else if (level1&&!level2) fLevel=60;
  else if (!level3) fLevel=90;
  else if (level1&&level2&&level3) fLevel=0;
  else fLevel=-1;
  if (fSavLevel!=fLevel) {
    Serial.printf("Level has changed its state: %.f\n", fLevel);
    fSavLevel = fLevel;
  }
}
Листинг 2. Установка режимов входов и цикл опрос датчиков уровня жидкости
#include <Arduino.h>

void setup() {
  // Инициализация последовательного порта
  Serial.begin(115200); 
// конфигурирование пинов датчика уровня
  pinMode(18, INPUT); pinMode(19, INPUT); pinMode(21, INPUT);
}	

unsigned long lastUpdateTimeLevel = 0;
const long updateIntervalLevel = 1000;
void loop() {
  if (millis() - lastUpdateTimeLevel >= updateIntervalLevel) {
    lastUpdateTimeLevel = millis();
    // читаем входы датчика уровня  
    void LevelView();
    LevelView();
  }
}

Поскольку мы используем редактор VSCode и платформу PlatformIO, то нужен еще файл инициализации проекта под стандартным именем platformio.ini, который определяет 1) текущую платформу, 2) тип платы, 3) параметры порта для подключения микроконтроллера 4) подключаемые библиотеки и другие параметры проекта. Его вид приведен на листинге 3.

Листинг 3. Файл инициализации проекта platform.ini
 [platformio]
default_envs = esp32dev

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200	

Вот фактически и весь проект, который далее мы будем считать исходным.

Преобразуем созданный классический проект к автоматному виду. Для этого нам необходимо: 1) подключить библиотеку, интерпретирующую автоматы, 2) создать и подключить к ядру автомат, реализующий работу с датчиками.  Подключение библиотеки к проекту демонстрирует листинг 4, где курсивом выделено дополнение к исходному варианту (ср. с листингом 3).

Листинг 4. Подключение библиотеки к проекту
 [platformio]
default_envs = esp32dev

 [env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps = https://github.com/lvs628/VCPa
monitor_speed = 115200

Рассмотрим превращение обычной функции (см. выше LevelView) в программный автомат. Граф автомата для работы с датчиком уровня приведен на рис. 2. Создан он с помощью Microsoft Visio и служит примером документирования автоматов. И если в исходном проекте мы исходно создавали функцию обслуживания датчиков, то в рамках АП мы изначально проектируем процесс в форме автомата.

Программный автомат — это процесс с заданным поведением. В проекте он анализирует величину заданной задержки, исполняет функции опроса входов микроконтроллера и создает, если задано, задержку между опросами. И все это, включая задержку, которая в этом случае тоже автомат, но только вложенный, реализуется автоматным ядром.

Рис.2. Автоматная модель управления датчиком уровня
Рис.2. Автоматная модель управления датчиком уровня
Рис.2. Автоматная модель управления датчиком уровня

Коды заголовка автоматного класса и его реализация приведены на листингах 5 и 6. Здесь курсивом выделено то, что отличает новую программу от ее исходного варианта, а дополнительно подчеркиванием то, что относится к специфике автоматной библиотеки. Данный объект инкапсулирует свойства, методы и поведение автоматного процесса. В этом — поведении и есть качественное отличие объектного автоматного подхода от классического.

В программировании микроконтроллеров обычно избегают применения ООП. Возможно, потому, что программированием занимаются те, кто лучше разбирается в железе и им ближе простой Си? Другой причиной может быть желание ужать код. И, конечно, отсутствие поведения у классических объектов не способствует их популярности у тех, кто много внимания уделяет «умным» алгоритмам, т. е. увлечен IoT. С автоматной библиотекой, как минимум, поведение у обычных объектов есть точно.

Листинг 5. Файл заголовка автомата датчика уровня
#ifndef __SENSORLEVEL_H
#define __SENSORLEVEL_H

#include <Arduino.h>
#include "lfsaappl.h"
class TAppProcesses;
class SensorLevel:public LFsaAppl 
{
public:
    void LevelView();
    SensorLevel(string strNam = "SENSORLEVEL");
    ~SensorLevel() {};
    // Номера выводов для цифровых входов сенсора
    const int gpioLevel1{18};
    const int gpioLevel2{19};
    const int gpioLevel3{21};
    int nDelay{0};
protected:    
    int x1(); int x2();
    void y1(); void y2(); void y3(); void y4();
    bool level1{false};
    bool level2{false};
    bool level3{false};
    bool bIfViewError{false};
    float fSavLevel{-1};
};
#endif // __SENSORLEVEL_H
Листинг 6. Файл реализации процесса контроля уровней жидкости
#include <Arduino.h>
#include "stdafx.h"
#include "SensorLevel.h"
LArc TBL_SENSORLEVEL[] = {
  LArc("ss",    "ss","^x1^x2",  "y1"), 
  LArc("ss",    "s1","x1",      "y1"), 
  LArc("ss",    "er","x2",      "y3"), 
  LArc("s1",    "ss","--",      "y2"),
  LArc("er",    "er","--",      "y4"),
  LArc()
};
SensorLevel::SensorLevel(string strNam):
  LFsaAppl(TBL_SENSORLEVEL, strNam)
{
// конфигурирование пинов датчика уровня
  pinMode(gpioLevel1, INPUT); pinMode(gpioLevel2, INPUT); 
  pinMode(gpioLevel3, INPUT); bIfViewError = false;
}
// ПРЕДИКАТЫ
int SensorLevel::x1() { return nDelay > 0; }
int SensorLevel::x2() { return nDelay < 0; }
// ДЕЙСТВИЯ
void SensorLevel::y1() { LevelView(); }
// создание задержки
void SensorLevel::y2() { FCreateDelay(nDelay); }
// ошибка в задании задержки
void SensorLevel::y3() { Serial.printf("%s(%s):error nDelay=%d\n", FGetNameVarFSA().c_str(), FGetState().c_str(), nDelay); }
void SensorLevel::y4() { 
  if (!bIfViewError) { Serial.printf("%s(%s)\n", FGetNameVarFSA().c_str(), FGetState().c_str()); bIfViewError = true; }
}
// Отображение состояния датчиков уровня жидкости
void SensorLevel::LevelView()
{
  level1 = digitalRead(gpioLevel1);
  level2 = digitalRead(gpioLevel2);
  level3 = digitalRead(gpioLevel3);
  if (!level1&&level2&&level3) fLevel=30;
  else if (level1&&!level2) fLevel=60;
  else if (!level3) fLevel=90;
  else if (level1&&level2&&level3) fLevel=0;
  else fLevel=-1;
  if (fSavLevel!=fLevel) {
    Serial.printf("%s:Level has changed its state: %.f\n", FGetNameVarFSA().c_str(), fLevel);
    fSavLevel = fLevel;
  }
}

Созданный автоматный код мало отличается от кода обычного классического объекта. Это видно по объему подчеркнутого текста. Здесь есть некий базовый класс — LFsaAppl, которому передается ссылка на таблицу переходов автомата — TBL_SENSORLEVEL. Присутствуют методы, которые ассоциируются с именами в данной таблице. Во всем этом чего‑то необычного для ООП нет.

Как все это работает? Имеется ядро в форме библиотеки, которому передается информация об автоматном объекте. За создание ядра, ссылок к нему и реализацию дискретного времени, отвечает код, представленный на листинге 7.

Листинг 7. Код реализации ядра и дискретного времени автоматов на ESP32
#include <Arduino.h>
#include "TAppProcesses.h"        // ядро FSM
void setup() {
  // Инициализация последовательного порта
  Serial.begin(115200); Serial.println();
  Serial.println("Demo project - SensorLevelFSM");
// создаем среду для автоматов
  VCPaCore_init();                // создание ядра FSM
// загружаем автоматные процессы  
  LoadingFsaProsesses();
  string str = "Setting1";
  CVarSetting *pViewVar = pTAppProcesses->pSetVarSetting->GetAddressVar(str);
  if (pViewVar) { pViewVar->dDeltaTime = 2; }
}
void loop() {
// моделирование дискретного времени FSM  
  VCPaCore_TimerEvent(0);
}

За создание автоматных объектов отвечает фунгция LoadingFsaProcesses(). Ее код приведен в листинге 8. Здесь демонстрируется создание только одного объекта автоматного типа. По аналогии можно создать его копии или добавить другие.

Листинг 8. Код подключения автоматного объекта к ядру VCPa
#include "TAppProcesses.h"
#include "SensorLevel.h"
SensorLevel *pSensorLevel{nullptr};             // ссылка на процесс 1
SensorLevel *pSensorLevel2{nullptr};            // ссылка на процесс 2
void LoadingFsaProsesses() {
    pSensorLevel = new SensorLevel("sensor1");  // создание объекта процесса
    string vFSA1 = "sensor1;0;0;0;0;InitFsaWorld;1;1";
    static CVarFSA var1(pTAppProcesses,vFSA1);  // описатель процесса
    bool bRet1 = var1.LoadFsa(pSensorLevel); // загрузка процесса
    if (bRet1) Serial.println("Load sensor1");
// устанавливаем задержки датчикам уровня    
    pSensorLevel->nDelay = 500;      // задержка датчика 1
//    pSensorLevel->FSetSleep(100);  // установка длительности такта процесса
    pSensorLevel2 = new SensorLevel("sensor2"); // создание объекта процесса
    string vFSA2 = "sensor2;0;0;0;0;InitFsaWorld;1;1";
    static CVarFSA var2(pTAppProcesses,vFSA2);   // описатель процесса
    bool bRet2 = var2.LoadFsa(pSensorLevel2); // загрузка процесса
    if (bRet2) Serial.println("Load sensor2");
// устанавливаем задержки датчикам уровня    
    pSensorLevel2->nDelay = -1;   // задержка датчика 2
}

В заключении осталось озвучить размер библиотеки. Для микроконтроллера ESP32 с объемом памяти 4Mb он чуть более 24%. Совсем не запредельный объем, если сравнивать с наиболее объемными библиотеками ESP32. Тем не менее, как показывает практика, лучше использовать конфигурации ESP32 с большим объемом оперативной памяти.

Выводы

Выше мы описали, как по большому счету изменится само программирование, использующее конечно‑автоматную форму вычислительных процессов. Причем любых — последовательных и параллельных. И, подчеркнем, не только на базе микроконтроллеров, а вообще везде. Цель, конечно, глобальная, но реализуемая. Тем более, когда она стала доступна микроконтроллерам!

Сама же реализация автоматного ядра вполне может быть другой. Хотелось бы видеть ее аппаратной, но на крайний случай это могут быть те же потоки. Однако параллелизм в одном потоке удобнее, надежнее и если уж совсем прямо — эффективнее. Так нет проблем с отладкой, с синхронизацией, гонками, производительностью и т. д. и т. п.

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

Современные микроконтроллеры и ПК прекрасно дополняют друг друга. Создается ситуация, когда среду проектирования для микроконтроллеров можно использовать лишь на финишном этапе, а саму разработку вести на ПК. Именно так в среде Qt Creator была создана и проверена наша библиотека. Это позволило многократно ускорить процесс проектирования. Да и, если честно, вообще осуществить саму идею переноса.

Необходимо упомянуть характеристики реального проекта, из которого заимствован рассмотренный выше пример. Кроме датчиков уровня жидкости он содержит датчики температуры, влажности, освещенности, реле и т. д. и т. п. Все процессы реализованы как автоматы, которые работают в жестком реальном времени (дискретность такта 10 мсек). Проект поддерживает подключение по WiFi, MQTT, Home Assistant и имеет свою страничку управления. При этом нет нужды во FreeRTOS фактически от слова совсем.

С учетом библиотек и прикладных процессов загрузка памяти на базе микроконтроллера ESP32-WROOM-32 приближается к 100%, где библиотеки занимают почти 85% из доступных 4Mb памяти (библиотека автоматов, напомним, занимает немногим более 24%) Поэтому в планах переход на микропроцессор ESP-32-S3-WROOM-1, который имеет на борту 16Mb. А этого уже достаточно для большинства более масштабных проектов в рамках технологии АП.

Вариант проекта на базе ESP32 под именем HotbedAgroControl, спроектированный по канонам АП, уже доступен на маркетплейсе OZON (https://ozon.ru/t/CKMsv9P). На его базе технология АП прошла многостороннюю проверку и обкатку теперь уже на микроконтроллерах. Правда, из‑за проблем с объемом памяти (см. замечание выше) в самом простом варианте (фактически в формате SWITCH‑технологии). Но даже в этом случае она подтвердила свою эффективность при решении проблем распараллеливания в жестком реальном времени.

Осталось сказать, что исходные коды библиотеки VCPa доступны на GitHub по адресу — https://github.com/lvs628/VCPa, где в папке examples находится и рассмотренный нами пример. Поскольку библиотека написана на С++ лишь с небольшими вкраплениями STL, то напрямую или после небольшой адаптации она может быть использована везде, где все это есть. А есть это везде или почти везде, если говорить о микроконтроллерах.

Литература
  1. ВКПа. Введение, ч.1. Визуальное проектирование автоматов. https://habr.com/ru/articles/794498/

  2. Автоматная модель управления программ. https://habr.com/ru/articles/484588/

  3. Модель параллельных вычислений. https://habr.com/ru/articles/486622/

  4. Автоматное программирование: определение, модель, реализация. https://habr.com/ru/articles/682422/

  5. Машина Тьюринга, как модель автоматных программ. https://habr.com/ru/articles/481998/

  6. Параллелизм, корутины, событийные автоматы,… живая математика. https://habr.com/ru/articles/499460/

  7. Параллелизм и эффективность: Python vs FSM. https://habr.com/ru/articles/506604/

  8. Мир без корутин. Итераторы-генераторы. https://habr.com/ru/articles/512348/

  9. Мир без корутин. Костыли для программиста — asyncio. https://habr.com/ru/articles/513512/

  10. Автоматное программирование в SimInTech и ВКПа. https://habr.com/ru/articles/717190/

  11. Практика применения автоматов в ПЛК. https://habr.com/ru/articles/694078/

  12. Йодан Э. Структурное проектирование и конструирование программ. М.: Мир,1979 — 415с

  13. Хамби Э. Программирование таблиц решений. М.: Мир, 1976. — 86 с.

  14. Зелковиц М., Шоу А., Геннон Дж. «Принципы разработки программного обеспечения». М.: Мир, 1982. –368 с.

  15. Толковый словарь по вычислительным системам/Под ред. В. Иллингоута и др.: Пер. с англ. А.К. Белоцкого и др.; Под ред. Масловского. — М.: Машиностроение, 1991. — 560 с.

Комментарии (63)


  1. ruomserg
    15.08.2025 09:45

    Конечные автоматы - это прекрасно. Я и сам их очень люблю. Но это ни разу не панацея. Во-первых, у вас существуют физически уникальные ресурсы (например, I2C шина). Исчезает независимость состояний - теперь придется думать и отлаживать целые кусты переходов с учетом общих ресурсов. Аналогично если появляется remove state - например, датчик в который надо сначала записать адрес, потом читать результаты... Во-вторых, сама форма для сложных процессов малопрактична. Сделать общение, например, с дисплеем 1604 через I2C регистр-расширитель - можно. Будет ли такой код красивее и понятнее чем через примитивы параллельного исполнения - ой, не знаю!

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

    Я помню, был какой-то фреймворк под MCU который продвигал идею автоматного программирования (и имел GUI для описания автоматов, и потом генерировал код). Чем кончилось - за давностью лет не помню.

    В общем, проклятие сложности никуда не девается. С одной стороны, перейдя на автоматы мы избавляемся от головной боли с гонками - ибо теперь можем 100% контролировать потоки выполнения. Но получаем ту же сложность, вылезающую в описании автоматов и размере таблиц переходов...


    1. lws0954 Автор
      15.08.2025 09:45

      Это приятно, что Вам нравятся автоматы. Но, судя по ответу, у Вас проблемы с их применением. И это понятно, т.к. автоматы они тоже разные. У Вас они явно другие. Например, какая бы ни была сложность системы, но «необозримая таблица переходов» - это не правильно. Наличие такого автомата – повод задуматься. Подобный автомат покрывается множеством (сетью) автоматов, каждый из которых имеет несравнимо меньшую таблицу переходов. Собственно так решается  упомянутое Вами «проклятие сложности». Т.е. получаем не «ту же сложность», а гораздо меньшую. Правда, появляется другое «проклятие» - создать подобную сеть ;) Но это  «проклятие», как правило, будет проще.

      Библиотека позволяет легко решать именно подобные проблемы.  И, что примечательно, без потоков, корутин и сопутствующих им «проклятий». Потоки и корутины сложность только множат, а предлагаемая библиотека ее уменьшает.


      1. ruomserg
        15.08.2025 09:45

        "Говорят, Рим строит дороги - да это ж мы, блдж, их строим!.." (C) Легионер с ютуба. Это я про "автомат покрывается сетью автоматов" - да это ж мы их покрываем!.. :-)

        Покажите вариант решения задачи на автоматах: на общей I2C шине (которая управляется аппаратным автоматом AVR (TWI) сидит драйвер дисплея HD44780 через I2C адаптер PCF8574. И на ней же сидит датчик влажности, которому надо для начала изменений записать константу, а через таймаут вычитать по I2C пять байт значений. Дисплей надо инициализировать в 4-битный режим, и в дальнейшем обновлять раз в секунду. Если он перестанет штатно отвечать - инициализацию надо повторить. Влажность можно считывать реже - скажем, раз в 30 секунд...

        Я очень хочу посмотреть на то, как вы этакую каракатицу легко опишите сетью автоматов. И насколько это будет легче читаемо чем решение через примитивы параллельного исполнения...


        1. lws0954 Автор
          15.08.2025 09:45

          Да легко! (Я так думаю!) Но Вы в теме, а я нет. А потому - алаверды. Представьте схематично (I2C и остальное - это все условности) свое решение в форме "примитивов параллельного исполнения", а я их легко, если не сказать - изящно ;), превращу в автоматы. Используя, конечно, библиотеку. Тогда и сравним.


          1. ruomserg
            15.08.2025 09:45

            Нет, у меня это есть на автоматах - поэтому я и спрашиваю. Оно сделано на автоматах не потому что это лучшее решение, а потому что по-другому в условиях ограниченных ресурсов не влезет. Был бы у меня под эту задачу Linux - я бы запустил задачи в разных процессах, и синхронизировал через семафор.

            Покажите на библиотеке достаточно сложный пример взаимодействия ? I2C - это очень распространенный стандарт с миллионом разных датчиков которые на нее вешаются. Интересно именно как вы собираетесь в ДКА синхронизировать работу с общим ресурсом, и как вы будете обслуживать транзакции на шине когда устройству надо послать несколько команд, проверить ответы, и т.д.

            Потому что рассказывать о преимуществах автоматного подхода в задаче чтения состояния ножки - это каждый может. Покажите насколько выразительно реализуются сложные протоколы взаимодействия в такой парадигме. HD44780 и PCF8574 можете посмотреть в библиотеках того же ардуино. Это как грязь распространенные микросхемы (точнее, их текущие китайские no-name аналоги), и найти на них документацию и примеры - не должно быть проблемой...


            1. lws0954 Автор
              15.08.2025 09:45

              Нет, у меня это есть на автоматах - поэтому я и спрашиваю.

              Так это ж совсем другое дело! :)

              Если Вы согласны, то мы сейчас и покажем/разберемся, что у Вас совсем другие автоматы:) Тогда и поймем, что и Linux , а семафоры так совсем зашквар :) По крайней мере у меня так. Когда есть библиотека VCPa, то не нужно ни то, ни другое.

              1. Код совсем не важен, но изобразите хоть как-нибудь автоматы. Лучше в форме графов, но можно и таблицы переходов. Входы пусть будут обозначены x1... xn, выходы - y1, y2, ... yn. В идеале должно походить (по форме, конечно) на мой автомат из статьи.

              2. Желательна и структурная схема. Она должна отражать количество автоматов и связи между ними. Что-то типа электронной схемы. Только здесь каждый автомат - это квадратик с входами и выходами, а между ними связи. Например (см. рис.2), в статье это один квадратик, имеющий один вход - x1 и два выхода - y1, y2, а внутри у него автомат.

              Сможете такое сделать? Можно начать со структурной схемы. Принимается любая форма. Хоть от руки. Главное принцип.


              1. ruomserg
                15.08.2025 09:45

                Не надо мне возвращать обезьянку. Это вы утверждаете что автоматная форма лучше/удобнее чем синхронизационные примитивы. Я, как человек, поевший и то и другое - утверждаю что это сильно не так (по крайней мере, сильно не везде и не всегда). В качестве модельной задачи я вам предлагаю вещь совершенно обыденную и широко распространенную: I2C шину и символьный дисплей (и любое простое устройство на ней же). Не хотите эту задачу - покажите решение любой другой, но аналогичной сложности. Чтение ног микроконтроллера меня лично не впечатляет.

                Дополнительно скажу, что вы не первый кто прибегает с идеей автоматного программирования. И почему-то все прибегают и показывают как она замечательно работает на простых примерах. К сожалению, на простых примерах вообще всё работает. Вы покажите на достаточно сложной реальной задаче!

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


                1. lws0954 Автор
                  15.08.2025 09:45

                  ...А я ради вашего удобства дополнительный код писать не собираюсь...

                  Так я ж просил не код...

                  Хорошо упростим задачу. Давайте оценим сложность Вашего проекта. У Вас проект на автоматах и это позволяет такую оценку сделать. А потом я приведу аналогичные цифры какого-нибудь из своих реальных проектов.

                  Итак, вопрос первый - сколько у Вас автоматов?

                  Вопрос второй - Сколько из них представляют параллельные процессы.

                  Вопрос третий - сколько состояний у каждого из этих автоматов.

                  Всего три цифры. Код писать не надо. Проект Ваш и он Вам понятен. Сделать, т.е. посчитать фактически, как мне кажется, не сложно.

                  Надеюсь, я Вас не сильно напряг? Я все сделал, чтобы Вам было как можно удобнее ;)


                  1. ruomserg
                    15.08.2025 09:45

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


                    1. lws0954 Автор
                      15.08.2025 09:45

                      А как мне оценить Ваш уровень? Вы не ответили ни на один мой вопрос. Вы не знаете сколько у Вас автоматов? Вы не знаете вообще сколько их у Вас. Вы не можете сказать сколько у них состояний. Что проще-то может быть, чтобы дать на них ответы? Да, может, Вы совсем не понимаете о каких автоматах идет речь? Может, и проекта такого (на автоматах) у Вас нет, а так ... одно только ля-ля? Давайте, наверное, как-то серьезнее относиться друг к другу... А просто "сотрясать воздух" смысла нет.

                      А ссылка на гит с библиотекой есть. В статье есть информация, как создавать автоматы и проекты на их базе. Что-то не ясно? Есть ссылка и на проект. Правда, в силу определенных причин его код не выложен пока на гит. Но это тоже будет сделано.


                      1. rukhi7
                        15.08.2025 09:45

                        Может, и проекта такого (на автоматах) у Вас нет,

                        а что такое автомат (он же КА) с точки зрения программирования, это же простой SWITCH, правильно? То есть по сути вы предлагаете программирование на SWITCH-ах? Вот очень интересно посмотреть конструкцию на SWITCH-е которая заменяет семафор, хотя я в принципе, теоритически, могу себе это представить, но ведь практическая ценность такой конструкции будет абсолютно нулевая? Не нужно больших проектов! Просто попробуйте изобразить такую конструкцию в коде.


                      1. lws0954 Автор
                        15.08.2025 09:45

                        Нет такой "точки зрения программирования". Но есть некая алгоритмическая модель, есть ее реализация в той или иной форме (графическая, текстовая и т.п.). Так вот, надо сравнивать модели, их "точки зрения". С такой "точки" SWITCH - не автомат. SWITCH не заменяет семафор. Наверное, его поведение и работу с ним можно как-то смоделировать автоматом/автоматами, но ... зачем? Как я уже сказал: нужен - используете "как есть". Какие проблемы? И это будет правильно. Зачем изобретать велосипед?


                      1. ruomserg
                        15.08.2025 09:45

                        Во-первых, вам не надо оценивать мой уровень. Это вы сюда пришли проповедовать автоматное программирование, и это мы будем оценивать полезность того, что вы предлагаете.

                        Во-вторых, моя гражданская специальность была 220100 - выч маш, системы и сети. Поэтому автоматы Мура/Мили, их перевод в переключательные функции, минимизация и синтез в базисе - это примерно наш хлеб с зимы первого по зиму третьего курса. Дальше начиналось микропрограммное управление и ассемблер...

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

                        А если вы только агитировать можете за коммунизм и автоматное программирование - ну тогда ой! С зелотами ругаться - только время терять...


                      1. lws0954 Автор
                        15.08.2025 09:45

                        Давайте жить дружно :) Но я не на экзамене, да и Вы тоже. Но мне хотелось бы оценить сложность Вашего существующего решения. Вы же оперируете понятием "сложного решения"? Для меня сложность оценивается количеством состояний автомата. Чем их больше - тем больше сложность. Какие системы Вы считаете сложными? С каким числом состояний? Зная это и бы мог предложить свое эквивалентное решение из уже существующих, чтобы Вы могли его заценить.

                        Реализовывать что-то другое? Зачем? У меня есть чем заняться. А как-то оценить проблему можно и абстрактно. Вам было предложено как это сделать, т.е. создать отдельны КА для шины и устройств на ней. Моя "замечательная библиотека" это позволяет сделать легко и просто. В чем проблема такого подхода? Можете ответить хоть в этот раз?

                        И я ни за что не агитирую. Я предложил конкретное решение. Оно в свободном доступе. У Вас достаточно (надеюсь) квалификации, чтобы его применить, Вы знаете "шину", Вы знаете "символьный дисплей", у Вас уже есть готовое решение, Вы даже знаете что такое автомат! Переложите свои автоматы на автоматы мои. Если Вас правильно учили то и мои и Ваши автоматы - это классические автоматы. Перевести одни в другие - тьфу! проблема. Ну и озвучьте, какие их проблем VCPa не решает? Ну, такой алгоритм должен быть общения между нормальными людьми. А если Вы помешаны на идеях коммунизма, круг Ваш общения - какое "золото", то - увольте. Мне бы хотелось больше про программирование. И лучше если с разных "точек зрения" на него. Или, что точнее, сравнивая разные его модели.


                      1. ruomserg
                        15.08.2025 09:45

                        То есть решения сколько-то похожей на реальную задачи на своих КА вы предъявить не только не можете, но даже и не собираетесь. Дальнейшая дискуссия смысла не имеет. Это будет спор о том, сколько ангелов помещается на кончике иглы. Идите со своими идеями к теологам, а Хабр - пока все-же технический ресурс...


                      1. lws0954 Автор
                        15.08.2025 09:45

                        Спора между нами нет и не было. Поскольку нет предмета самого спора, а есть одни некие "предъявы". Вы достали меня своей тупизной и нежеланием ответить на элементарные вопросы, чтобы создался хоть какой-то предмет спора. А попросту, по-видимому, Вы и не знаете на них ответа.Т.е. оценить сложность своего проекта Вы не в силах.

                        Хорошо, скорее не для Вас, а дех кто по наивности своей предполагает, что Вы из себя что-то представляете. Привожу скрины одного из своих реальных проектов. Это установка водоподготовки размером с железнодорожный вагон. Забита всякой всячиной, но для управления достаточно одного ПЛК. Разработка велась не на ПЛК, а путем моделирования ее работы на ПК. Приведена структурная схема, где каждый квадрат - это автомат. На схеме отражены все связи процессов. Своего рода пример нынешнего бэкенда. Мнемосхема - каждый ее элемент - это, порой, даже не один автомат. Можно сказать пример фронтенда. После окончания тестирования модели, с участием заказчика и под его контролем, все автоматы бэкенда перенесены были на ПЛК за один день и заработали сразу и без какой-либо отладки. Вагон был поставлен на рельсы и уехал туда, куда было ему нужно. Да было составлено полное описание алгоритмов работы всех элементов, представленных на структурной схеме. Что-то порядка ста листов документации. Работа прошла дружно и без каких-либо претензий. Приятно вспомнить:)

                        Структурная схема и мнемосхема модели установки


        1. Siemargl
          15.08.2025 09:45

          Картинка выглядит достаточно подходящей для КА. На каждое устройство свой КА.

          Параллельное исполнение и машина состояний понятия ортогональные.

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


          1. ruomserg
            15.08.2025 09:45

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

            Я не говорю, что это нерешаемая проблема - я хочу посмотреть, как конкретно этот адепт автоматного программирования хочет их решать. И насколько это решение хуже/лучше отдельных процессов/потоков на каждое из устройств с примитивами синхронизации...


            1. Siemargl
              15.08.2025 09:45

              я собственно про то, что теория КА не противоречит параллельности в любом виде.

              Но как всегда, КА лучше привязывать к экземпляру устройства. У нас общая шина на 10 приборов, значит КА на шину, а приборы пойдут подчиненными КА или просто параметрами.

              сихронизация зависимых КА дело муторное, надо думать над удобным разбиением и минимизацией количества


              1. lws0954 Автор
                15.08.2025 09:45

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


            1. lws0954 Автор
              15.08.2025 09:45

              Я не говорю, что это нерешаемая проблема - я хочу посмотреть, как конкретно этот адепт автоматного программирования хочет их решать. 

              "Адепт" подобные задачи решает "по щелчку"! Имея доступ к состояниям, он знает кто и что делает, а потому, используя эту информацию, может синхронизировать процессы так, как того требуют протоколы и нюансы их работы. Ну нет тут с автоматами проблем от слова совсем.


              1. ruomserg
                15.08.2025 09:45

                Вы код покажите, пожалуйста. А то будет как в анекдоте: "... а мой сосед говорит, что три раза за ночь может! // Ну так и вы говорите!..".


          1. lws0954 Автор
            15.08.2025 09:45

            Почти то, что надо. Вот только семафоры, думаю, ни к чему. Синхронизацию можно организовать через состояния. Это будет надежнее. Хотя - если хочется, то почему бы и нет. Автоматы семафоры не отрицают.


            1. Siemargl
              15.08.2025 09:45

              семафор это и есть атомарное состояние


              1. lws0954 Автор
                15.08.2025 09:45

                Семафор - это, скорее, объект операции с которым атомарны. Вот, кстати, что интересно, в параллельной библиотеке VCPa фактически все действия ее автоматов в определенном смысле атомарны. В чем заключается такая атомарность? В том, что ни один другой автомат не может прервать/вмешаться в "атомарное действие" текущего автомата. Ну, также, как в корутинах/сопрограммах (см. рис.1)


  1. rukhi7
    15.08.2025 09:45

    вот такие шедевры я прям люблю:

      level1 = digitalRead(gpioLevel1);
      level2 = digitalRead(gpioLevel2);
      level3 = digitalRead(gpioLevel3);

    вот прям никак не возможно прочитать порт одной командой? Или это специально развели на разные порты, чтобы скучно не было? (причем и тем кто разводит и тем кто это программирует) Или это просто Ардуино-стиль такой, так просто модно?

    если что вот это :

    if (!level1&&level2&&level3) fLevel=30;

    можно записать так:

    if (levels == 0x3) fLevel=30;

    но я наверно в другом мире живу, не одухотворен Ардуино.


    1. Zenitchik
      15.08.2025 09:45

      Или это просто Ардуино-стиль такой, так просто модно?

      Как я понял, да. Это стиль "для тупых". Типа, чтобы даже нуб понял. Ну, как мы в детстве на BASICe писали.

      А сам по себе микроконтроллер - нормальный. Всё в нём можно, что положено микроконтроллеру. И в самой среде Ардуино можно писать нормально.


      1. lws0954 Автор
        15.08.2025 09:45

        По поводу "нормально". Понятие растяжимое, хотя в статье прямо сказано - С++ и STL. Может, на Arduino это тоже уже есть? "Кому и кобыла невеста". Так, кажется, говаривал один персонаж из "12 стульев"?


        1. ruomserg
          15.08.2025 09:45

          Ну вообще-то кодогенерация для Ардуино - это обычный gcc/g++. Все что там есть - всё доступно... Другое дело что под какую-нибудь ATTINY13 писать на C++ с темплейтами и исключениями - довольно странно, да можно и в память не войти!


          1. lws0954 Автор
            15.08.2025 09:45

            У Arduino серьезные ограничения с С++. Как бы это было именно так. И именно не так давно. Неужели что-то изменилось?


            1. ruomserg
              15.08.2025 09:45

              Не надо говорить "Arduino", надо говорить о конкретном семействе микроконтроллеров. Если мы имеем в виду классическое ардуино - то это AVR. Никаких ограничений по кодогенерации там нет. По стандартной библиотеке - да, есть (с учетом того что оригинальная libc была для POSIX-систем, чего же вы хотите от MCU?!). По архитектуре памяти (например, вам надо заранее решить - хотите ли вы чтобы константа лежала в .data и занимала RAM или в .flash - но тогда использовать специальные функции чтобы ее доставать) - тоже есть. Но никаких ограничений именно по стандарту языка - я не помню. Пока вы влазите в память и стек - творите что хотите!


    1. lws0954 Автор
      15.08.2025 09:45

      Не придирайтесь :) Да, конечно, можно и подключить к портам и читать их как угодно. Вариантов тут множество. Но речь о другом. Дело не в реализации какого-то действия (чтения тех же портов), а в 1) в проектировании логики одного процесса и 2) в проектировании логики множества процессов.


  1. Sensimilla
    15.08.2025 09:45

    Насколько я понял, основной движущей силой проекта было - реализовать параллельность и ни в коем случае не столкнуться с RTOS. Может я незаслуженно опримитивливаю проект этим выводом, но я правда не понимаю. RTOS - это не только накладные расходы. ESP - контроллеры двухядерные, а RTOS необходима для эффективной загрузки обоих ядер. Ваш контроллер автоматов работает на одном ядре?


    1. lws0954 Автор
      15.08.2025 09:45

      Да, на одном. Но ни что не мешает использовать и второе ядро. Более того, можете даже использовать и RTOS. Без проблем. Каких-либо на использование чего-то иного ограничений нет.


  1. Dhwtj
    15.08.2025 09:45

    Довести идею корутин на базе автоматов до рабочего состояние поможет объектно-ориентированное программирование (ООП)

    Rust имеет встроенные конечные автоматы и обходится без ООП. Для Ардуино вполне вариант


    1. lws0954 Автор
      15.08.2025 09:45

      А можете на Rust создать такой же автомат, как и в статье? Сравним, что и как.


      1. Dhwtj
        15.08.2025 09:45

        Ну вот что-то близкое, но на раст и с конечным автоматом

        use std::time::Instant;
        
        // === Состояние сигнала на пине ===
        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
        pub enum Level {
            Low,  // 0V — датчик не активен
            High, // 3.3V/5V — датчик активен
        }
        
        impl Level {
            // Удобный метод: true, если High
            fn is_high(self) -> bool { self == Level::High }
            fn is_low(self)  -> bool { self == Level::Low }
        }
        
        // === Состояния конечного автомата ===
        #[derive(Debug, Clone, PartialEq)]
        enum State {
            Ss,
            S1,
            Er,
        }
        
        // === Датчик уровня с конечным автоматом ===
        struct SensorLevel {
            name: String,
            state: State,
            level1: Level,
            level2: Level,
            level3: Level,
            n_delay: i32,           // задержка: >0 — норма, <0 — ошибка
            f_level: f32,           // текущий уровень жидкости: 0, 30, 60, 90
            f_sav_level: f32,       // предыдущее значение уровня
            b_if_view_error: bool,  // флаг: ошибка уже показана
            delay_start: Option<Instant>, // начало отсчёта задержки
        }
        
        impl SensorLevel {
            fn new(name: &str) -> Self {
                Self {
                    name: name.to_string(),
                    state: State::Ss,
                    level1: Level::Low,
                    level2: Level::Low,
                    level3: Level::Low,
                    n_delay: 0,
                    f_level: -1.0,
                    f_sav_level: -1.0,
                    b_if_view_error: false,
                    delay_start: None,
                }
            }
        
            // === ПРЕДИКАТЫ ===
            fn x1(&self) -> bool { self.n_delay > 0 }
            fn x2(&self) -> bool { self.n_delay < 0 }
        
            // === ДЕЙСТВИЯ ===
            fn y1(&mut self) {
                self.level_view();
            }
        
            fn y2(&mut self) {
                println!("{}: Creating delay of {} ms", self.name, self.n_delay);
                self.delay_start = Some(Instant::now());
            }
        
            fn y3(&self) {
                println!("{}(ss): error nDelay={}", self.name, self.n_delay);
            }
        
            fn y4(&mut self) {
                if !self.b_if_view_error {
                    println!("{}(er)", self.name);
                    self.b_if_view_error = true;
                }
            }
        
            // === Определение уровня жидкости по датчикам ===
            fn level_view(&mut self) {
                use Level::*;
        
                let (l1, l2, l3) = (self.level1, self.level2, self.level3);
        
                let f_level = match (l1, l2, l3) {
                    (Low, High, High) => 30.0,
                    (High, Low, _)    => 60.0,
                    (_, _, Low)       => 90.0,
                    (High, High, High) => 0.0,
                    _ => -1.0, // нестабильное состояние
                };
        
                if self.f_sav_level != f_level {
                    println!("{}: Level has changed its state: {:.0}", self.name, f_level);
                    self.f_sav_level = f_level;
                }
                self.f_level = f_level;
            }
        
            // === Обновление значений датчиков (имитация digitalRead) ===
            pub fn update_sensors(&mut self, l1: Level, l2: Level, l3: Level) {
                self.level1 = l1;
                self.level2 = l2;
                self.level3 = l3;
            }
        
            // === Основной шаг автомата ===
            pub fn step(&mut self) {
                // Если мы в состоянии S1 — запущена задержка
                if let (State::S1, Some(start)) = (&self.state, self.delay_start) {
                    let elapsed = start.elapsed().as_millis() as i32;
                    if elapsed >= self.n_delay.abs() {
                        self.state = State::Ss;
                        self.delay_start = None;
                    }
                    return;
                }
        
                // Определяем входы
                let input_x1 = self.x1();
                let input_x2 = self.x2();
        
                // === Таблица переходов (как в TBL_SENSORLEVEL) ===
                match (&self.state, input_x1, input_x2) {
                    // ss --x1--> ss или s1, действие y1
                    (State::Ss, true, _) => {
                        self.y1();
                        // Например, переходим в s1
                        self.state = State::S1;
                    }
        
                    // ss --x2--> er, действие y3
                    (State::Ss, _, true) => {
                        self.y3();
                        self.state = State::Er;
                    }
        
                    // er -- любое --, действие y4 (один раз)
                    (State::Er, _, _) => {
                        self.y4();
                    }
        
                    // s1 -- без условия --> ждём, действие y2 (один раз при входе)
                    (State::S1, _, _) => {
                        // y2 выполняется при первом входе в S1
                        if self.delay_start.is_none() {
                            self.y2();
                        }
                    }
        
                    _ => {}
                }
            }
        }
        
        // === Пример использования ===
        fn main() {
            let mut sensor = SensorLevel::new("TankLevel");
        
            // Устанавливаем нормальную задержку
            sensor.n_delay = 1000; // 1 секунда
        
            // Цикл, как в Arduino
            for i in 0..15 {
                println!("\n--- Step {} ---", i);
        
                // Имитация изменения уровня жидкости
                let l1 = if i > 2 { Level::High } else { Level::Low };
                let l2 = if i > 5 { Level::High } else { Level::Low };
                let l3 = if i > 8 { Level::High } else { Level::Low };
        
                sensor.update_sensors(l1, l2, l3);
        
                // Выполняем шаг автомата
                sensor.step();
        
                std::thread::sleep(std::time::Duration::from_millis(600));
            }
        
            // Тест ошибки: отрицательная задержка
            println!("\n--- Test: Negative nDelay (Error) ---");
            sensor.n_delay = -500;
            sensor.state = State::Ss;
            sensor.step();
        }

        Кода больше, но он понятный


        1. Dhwtj
          15.08.2025 09:45

          Вот, разобрался с io

          //! # Монитор уровня жидкости на ESP32
          //! Простой, надёжный, легко читаемый код без излишеств.
          
          use esp_idf_hal::gpio::{Gpio0, Gpio1, Gpio2, Input, Pin};
          use esp_idf_hal::peripherals::Peripherals;
          use esp_idf_hal::prelude::*;
          use std::time::Instant;
          
          fn main() -> anyhow::Result<()> {
              // —————————————————————————————
              // 1. Инициализация оборудования
              // —————————————————————————————
              let peripherals = Peripherals::take()?;
              let pins = peripherals.pins;
          
              // Настроим GPIO как входы
              let sensor_low  = pins.gpio0.into_input()?;
              let sensor_mid  = pins.gpio1.into_input()?;
              let sensor_high = pins.gpio2.into_input()?;
          
              // —————————————————————————————
              // 2. Переменные состояния
              // —————————————————————————————
              let mut last_level: f32 = -1.0;        // Предыдущий уровень
              let mut delay_start: Option<Instant> = None; // Таймер задержки
              let delay_ms: i32 = 1000;               // Задержка: >0 — норма, <0 — ошибка
              let mut error_shown = false;            // Флаг: ошибка уже показана
          
              // Приветствие
              println!("[TankLevel] Система запущена");
          
              // —————————————————————————————
              // 3. Основной цикл
              // —————————————————————————————
              loop {
                  // —— Чтение датчиков ——
                  let l1 = read_gpio(&sensor_low);
                  let l2 = read_gpio(&sensor_mid);
                  let l3 = read_gpio(&sensor_high);
          
                  // —— Определение уровня жидкости ——
                  let current_level = match (l1, l2, l3) {
                      (false, true,  true)  => 30.0,  // Только средний и верхний — 30%
                      (true,  false, _)     => 60.0,  // Нижний и средний (но не верх) — 60%
                      (_,     _,     false) => 90.0,  // Верхний не сработал — 90%+
                      (true,  true,  true)  => 0.0,   // Все сработали — пусто
                      _ => -1.0,                      // Некорректное состояние
                  };
          
                  // —— Уведомление при изменении уровня ——
                  if (last_level - current_level).abs() > f32::EPSILON {
                      match current_level {
                          0.0..=100.0 => println!("[Level] Уровень: {:.0}%", current_level),
                          _ => println!("[Level] ОШИБКА: некорректные показания датчиков"),
                      }
                      last_level = current_level;
                  }
          
                  // —— Обработка задержки (например, для активации насоса) ——
                  if delay_ms > 0 {
                      manage_delay(delay_ms, &mut delay_start);
                  } else if !error_shown {
                      println!("[ERROR] Некорректная задержка: {}", delay_ms);
                      error_shown = true;
                  }
          
                  // —— Пауза между опросами ——
                  std::thread::sleep(std::time::Duration::from_millis(500));
              }
          }
          
          // —————————————————————————————
          // Вспомогательные функции
          // —————————————————————————————
          
          /// Читает состояние GPIO, обрабатывая возможные ошибки.
          fn read_gpio(pin: &impl InputPin) -> bool {
              pin.is_high().unwrap_or(false)
          }
          
          /// Управление таймером задержки: запуск и завершение.
          fn manage_delay(delay_ms: i32, delay_start: &mut Option<Instant>) {
              if delay_start.is_none() {
                  println!("[Delay] Запуск: {} мс", delay_ms);
                  *delay_start = Some(Instant::now());
              }
          
              if let Some(start) = *delay_start {
                  if start.elapsed().as_millis() >= delay_ms as u128 {
                      println!("[Delay] Завершено");
                      *delay_start = None;
                  }
              }
          }


          1. Dhwtj
            15.08.2025 09:45

            Должно быть

            let current_level = match (bottom, mid, high) {
                (false, false, false) => 0.0,   // Все сухие → пусто
                (true,  false, false) => 30.0,  // Только нижний в воде
                (true,  true,  false)  => 60.0,  // Ниже верхнего
                (true,  true,  true)   => 100.0, // Все в воде → полон
                _ => -1.0,                       // Ошибка: например, (false, true, true)
            };

            А у вас, пардон, фигня. Сразу и не заметил


            1. lws0954 Автор
              15.08.2025 09:45

              Ни разу ни фигня :) Просто здесь я одним датчиком проверяю работу автомата. Для трех я сделаю правильно. А Вам спасибо и за код и за внимательное отношение к статье ;)


              1. Dhwtj
                15.08.2025 09:45

                В любом случае через match читается лучше и нельзя потерять не валидный случай


                1. Siemargl
                  15.08.2025 09:45

                  Это конечно все хорошо, но по факту надо ещё учитывать градиент. т. е уровень понизился и в диапазоне 60..30 (ближе к 60) или повышался и в диапазоне 30..60 (ех 32)

                  состояние датчиков при этом одинаковое, но объёмы сильно разные


  1. eao197
    15.08.2025 09:45

    Сложилось впечатление, что содержание статьи ни разу не соответствует заголовку. Тема параллелизма не раскрыта от слова совсем.


    1. lws0954 Автор
      15.08.2025 09:45

      А у меня сложилось впечатление, что Вы не читали статью. Все же она не про параллелизм, как таковой, а про то, как избавиться от "параллелизма" потоков и корутин. Про библиотеку VCPa. Но, если Вас интересует теория АП, то пжалте пройтись по моим ссылкам и литературе (специально вставлены для таких как Вы). Там про автоматный параллелизм все очень подробно. Про потоки и корутины читайте статьи на Хабре других авторов. Они "коротко" или не очень :) тоже все рассказали.

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

      Вот так. Будьте внимательны. Успехов!


      1. eao197
        15.08.2025 09:45

        Все же она не про параллелизм, как таковой, а про то, как избавиться от "параллелизма" потоков и корутин.

        Смотрим на название статьи: "Как избежать кошмара параллелизма в IoT: автоматы вместо потоков и корутин"

        Т.е. в заголовке вы обозначаете наличие некого "кошмара параллелизма" и обещаете рассказать про некий способ его избежать. В самой же статье сперва очень много наукообразных слов о том, что считалось короутинами в 1970-х. И не менее наукообразные, но совершенно бесполезные рассуждения о КА из тех же самых, если не более ранних времен. Потом еще немного про какой-то стремный оторванный от жизни пример с сылками на еще более стремную в своей реализации библиотеку. И все.

        Поэтому хочется спросить: о каком именно кошмаре параллелизма вы собирались нам, читателям, рассказать?

        Или заголовок следует воспринимать как чистой воды кликбейт?


        1. lws0954 Автор
          15.08.2025 09:45

          Перечитайте еще раз тот мой пост из которого Вы заимствовали цитату. Повторяю для непонятливых: про сами кошмары - читайте других авторов. Или Вы считаете, что их нет? Если так, то Вы - наивный программист, верящий в нечто, что (потоки, корутины), как минимум, многими признается "стремным". Еще раз - читайте, читайте и читайте... Ссылки Вам даны. Время, надеюсь, найдете. Иначе, если не было, то "кошмары" Вас обязательно настигнут :)

          Когда захотите от них избавиться - купите микроконтроллер ESP32 (что совсем дешево) и на него установите библиотеку (еще дешевле - бесплатно), которая Вас от них избавит. Перечитайте статью, которая об этом повествует (как пользоваться библиотекой). Тоже затрат не требуется: по времени всего-то 17 мин (если верить Хабру) Разжевано и распихано по клювикам... ;). Поэтому при минимуме затрат сможете получить, возможно, максимум удовольствия :) Других вариантов избавиться от существующих "кошмаров", увы, нет. Еще раз - успехов!

          Да и есть совсем практичный вариант (см. в статье про HotbedAgroControl). Чуток подороже правда, будет, но зато все в одном флаконе - и микропроцессор и практический пример (некоторым просто вынь да положь!) применения автоматов. Короче - осваивай - не хочу! Практично, экономично, фантастично и без кошмаров! :) "Как вам такое, Илон Маск!"

          И, да, может, показаться, что кликбейт. Но совсем нет. Читайте, думайте, все открыто, все можно попробовать. Все или халявно или почти халявно. Но уж совсем наглеть не надо-то!? Так что, какой там кликбейт - упаси Бог! :)


          1. eao197
            15.08.2025 09:45

            Перечитайте еще раз тот мой пост из которого Вы заимствовали цитату.

            Обычно за разгребание чужого говна мне платят. А вы хотите, чтобы я тратил на это время бесплатно? o_O

            про сами кошмары - читайте других авторов

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

            Пальцем ткните, пожалуйста. Мол, вот здесь проблема, вот мое решение.

            Или Вы считаете, что их нет?

            Сходу могу назвать один, главный -- разделяемое между параллельными задачами мутабельное состояние. Только вот в статье о нем что-то ничего не увидел. Может укажите на конкретный абзац или раздел?


            1. lws0954 Автор
              15.08.2025 09:45

              С интересом прочитал про это самое состояние - https://ru.hexlet.io/blog/posts/which-skills-to-develop. Действительно, такие есть? Не зря его назвали "мутабельным" :)

              Так вот, спешу Вас огорчить в АП (автоматное программирование) оно в принципе невозможно. По теории и практике АП. Я так понимаю основная его проблема не в том, что оно глобально, не в том, что его отслеживать как-то сложно, а в его неповторяемости. И это понятно. Нет детерминизма у потоков. Оттого и проблема. И у сетей автоматов есть глобальное состояние. Это текущее состояние все состояний его автоматов. И тут полный контроль и повторяемость. Как то так. Надеюсь, теперь понятно, как избавиться от "кошмара мутабельности"? :)

              Да, если я порешал Вашу проблему, то, может, и Вы мне чего-нибудь подбросите. Как никак я, может, тоже помог Вам что-то "разгребсти". Кстати, и мне за это обычно платят. Вот совпадение-то!


              1. eao197
                15.08.2025 09:45

                С интересом прочитал про это самое состояние - https://ru.hexlet.io/blog/posts/which-skills-to-develop

                Ваша ссылка ведет на статью под названием "Какие навыки необходимы на разных этапах карьеры". Вы ошиблись или реально искали там определение "разделяемое мутабельное состояние"?

                Надеюсь, теперь понятно, как избавиться от "кошмара мутабельности"? :)

                Нет. Не понято. Как и непонятно можете ли вы вообще связно излагать свои мысли на простом русском языке.

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

                "Где связь между вашим наукообразным потоком сознания и "кошмарами", описанными в других статьях других авторов?

                Пальцем ткните, пожалуйста. Мол, вот здесь проблема, вот мое решение."


                1. lws0954 Автор
                  15.08.2025 09:45

                  Извиняйте. А вот так - https://ru.hexlet.io/blog/posts/global-mutable-state? Подходит? Но, с другой стороны, ссылка, конечно, не та, но чем Вас не устроил мой ответ. По сути я ответил или нет?


                  1. eao197
                    15.08.2025 09:45

                    А вот так - https://ru.hexlet.io/blog/posts/global-mutable-state?

                    Так да. Но я не говорил про "глобальное", я говорил про разделяемое мутабельное состояние. Оно не обязательно должно быть глобальным.

                    но чем Вас не устроил мой ответ

                    Во-первых, левой ссылкой.
                    Во-вторых, отсутствием вменяемых пояснений по поводу того, как КА спасают от проблемы разделяемого мутабельного состояния при параллельном программировании.
                    В-третьих, отсутствием ответов на ранее заданные вопросы.

                    что такое сеть автоматов, что такое общее состояние всех автоматов?

                    Вот это вот нуждается в расшифровке. Особенно "общее состояние" для параллельно и независимо работающих КА.

                    Кроме того, у меня сложилось ощущение, что под "параллельностью" вы понимаете что-то свое, известное только вам.

                    И еще одно ощущение: вы не сможете объяснить что лично вы вкладываете в понятие "параллельность".


                    1. lws0954 Автор
                      15.08.2025 09:45

                      Решил поискать... Сразу наткнулся на автора - Модель Акторов и C++: что, зачем и как? Похоже Ваше. И вы с 2017 года так и не разобрались с мутабельностью? Разделяемой само-собой. Жуть и кошмар.

                      И на какие я вопросы не ответил? На глупые, наверное? Так и не собираюсь. Потому как хочешь уточнить - молчок...

                      По поводу параллельно и независимо. Так это совсем фигня. Интересны именно взаимозависимые. С ними самая беда. Только что тут уточнять? Все следует из теории. Есть такая - алгебра автоматов, где рассматриваются разные варианты соединения автоматов. Читаем букварь и начинаем понимать, если на пальцах не понятно.

                      По параллельность - все из той же теории автоматов. Читайте, вникайте, если на слух не получается понять. Я ж когда-то с этим разобрался, надеюсь, и Вы осилите. Но проще почитать мои статьи на тему теории АП. Там выжимки того, что надо. Ссылки есть. Нужно только пальчиком ткнуть. Конечно я что-то добавил свое. Применительно к программированию. Поэтому советую мои, чтобы не корячиться и идти моим путем, т.к. классическая теория автоматов - это одно, а применительно к программированию - есть нюансы. Будете повежливее - расскажу, нет - ковыряйтесь сами (с усами).

                      Т.е. читаем теорию автоматного программирования и - в путь. Без всякой мутабельности, но зато параллельно по-настоящему. Все Ваши с мутабельностью беды от "многия знаний". Причем без знания каких-либо основ теории - в программировании, в кибернетике, теории цифровых схем и .д. и т.п. Манагеры он такие... Думают, все можно взять нахрапом, наскоком, ... мутабельностью, так сказать. Сначала создадут проблему, а потом с ней борются всем кагалом. А нет, чтобы тормознуть и подумать, заглянуть в книжки. Может, даже старые...

                      "Мутабельное" - придумают же! В приличном обществе и сказать-то как-то неудобно :) Разберитесь с автоматами и выражаться не прийдется :)


                      1. eao197
                        15.08.2025 09:45

                        И на какие я вопросы не ответил?

                        На все вышеперечисленные.

                        Но проще почитать мои статьи на тему теории АП

                        Ваши статьи -- редкостное графоманство, написанное нечитаемым наукообразным языком.

                        А в комментариях вы ведете себя как высокомерный чудак на букву "М", который не может ответить на прямые простые вопросы прикрываясь мантрой "идите читайте".


                    1. Siemargl
                      15.08.2025 09:45

                      как КА спасают от проблемы разделяемого мутабельного состояния при параллельном программировании.

                      КА инкапсулируют разледеляемый ресурс - свое состояние.

                      Особенно "общее состояние" 

                      Это множество текущих состояний всех КА.

                      В общем, сама идея интересная, но статья увы и ах. Параллелить КА можно как yield на состоянии ожидания, так и удобнее между потоками.

                      Как при этом у автора набегают мегабайты объектного кода, большая загадка


                      1. eao197
                        15.08.2025 09:45

                        КА инкапсулируют разледеляемый ресурс - свое состояние.

                        Давайте попробуем более предметно и менее абстрактно.

                        Допустим, что у нас есть электронная табличка. По типу Google Sheets.
                        С одной таблицей могут одновременно работать несколько пользователей.
                        Допустим, работа каждого пользователя описывается своим КА.

                        Что с данными этой таблицы?

                        Два пользователя могут одновременно вводить в нее информацию. Причем, если они вносят новые значения в разные места таблицы, то делать они это могут реально в параллель.

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

                        Или еще интереснее: один пользователь вводит данные в свою ячейку, которую сейчас не трогает пользователь №2. Но изменение ведет к пересчету значения в другой ячейки, которая сейчас как раз пользователю №2 нужна.

                        Если представлять данные таблицы в виде своего КА, то придется все запросы (на модификацию, на чтение и т.д.) сериализовать в последовательность запросов. Которая и обрабатываться будет последовательно. Что как бы не очень хорошо влияет на "параллельность".


                      1. Siemargl
                        15.08.2025 09:45

                        Допустим, работа каждого пользователя описывается своим КА.

                        КА надо делать на ресурс. в данном случае ресурс это таблица.

                        Да, неудобный выбор КА приводит только к усложнению логики.

                        Проблема очереди изменений типичная для СУБД.

                        Все таки этот пример далековато от темы статьи - контроллеры и устройства, где КА уместнее


                      1. eao197
                        15.08.2025 09:45

                        КА надо делать на ресурс.

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

                        Все таки этот пример далековато от темы статьи - контроллеры и устройства, где КА уместнее

                        Я долго не мог понять о какой параллельности говорит автор, смутило то, что статья помещена в хаб "параллельное программирование". Для меня параллельное программирование -- это parallel computing. Тогда как автор, видимо, говорит о параллельной связанности КА, что вовсе не обязательно должно означать параллельное исполнение самих КА (запросто может быть и всего лишь квазипараллелизм).


                      1. Siemargl
                        15.08.2025 09:45

                        не скажу за автора, но для меня интересно запустить каждый КА в своём треде и пусть уже библиотека раскидывает между ядрами и ожиданиями на не самом мощном эмбед железе


                      1. lws0954 Автор
                        15.08.2025 09:45

                        Еще раз. Модель, модель и еще раз модель. Какую модель параллельных вычислений реализует данный подход? Прежде чем что-то предлагать нужно дать ответ на вопрос - какая модель реализуется?


                      1. eao197
                        15.08.2025 09:45

                        Использую этот подход (но не в embedded) много лет. Он хорош, но далеко не всегда, увы, в области concurrent computing. А в parallel computing, имхо, мало применим.


                      1. lws0954 Автор
                        15.08.2025 09:45

                        Для меня параллельное программирование -- это parallel computing.

                        Это для Вас и для тех, кто писал материал, на который указывает Ваша ссылка. А теоретически правильное определение: параллельное программирование - это проектирование параллельных алгоритмов в рамках параллельной модели. Ключевые термины - параллельный алгоритм и параллельная модель вычислений. Это "азбука", которую следует знать. Нет параллельной модели - нет параллельных вычислений. Люди рассуждают, не зная "азбуки". Из-за этого и все проблемы типа ... "мутабельности".


                      1. eao197
                        15.08.2025 09:45

                        Вы откровенно утомили своей манерой разговора в стиле "Я уже познал дзен, а вы читайте, читайте, читайте, все ссылки я дал". Когда это все множится на ваши говнотексты и суммируется с говнокодом в продвигаемой вами библиотеке, то смесь получается... Своеобразная, мягко говоря.


                1. lws0954 Автор
                  15.08.2025 09:45

                  А здесь - https://ru.hexlet.io/blog/posts/global-mutable-state? Но в любом случае я ответил. Или Вам нужно разжевывать, что такое автомат, что такое его состояние, что такое сеть автоматов, что такое общее состояние всех автоматов? Такой язык Вам заходит? Я бесплатным образованием не занимаюсь, однако.