Комментарии 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, то не нужно ни то, ни другое.
Код совсем не важен, но изобразите хоть как-нибудь автоматы. Лучше в форме графов, но можно и таблицы переходов. Входы пусть будут обозначены x1... xn, выходы - y1, y2, ... yn. В идеале должно походить (по форме, конечно) на мой автомат из статьи.
Желательна и структурная схема. Она должна отражать количество автоматов и связи между ними. Что-то типа электронной схемы. Только здесь каждый автомат - это квадратик с входами и выходами, а между ними связи. Например (см. рис.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 необходима для эффективной загрузки обоих ядер. Ваш контроллер автоматов работает на одном ядре?
Довести идею корутин на базе автоматов до рабочего состояние поможет объектно-ориентированное программирование (ООП)
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)
};
А у вас, пардон, фигня. Сразу и не заметил
Как избежать кошмара параллелизма в IoT: автоматы вместо потоков и корутин