Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиСредний
Время на прочтение17 мин
Количество просмотров744
Всего голосов 2: ↑2 и ↓0+2
Комментарии30

Комментарии 30

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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();
}

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

Вот, разобрался с 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;
        }
    }
}

Должно быть

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)
};

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации