1. Тема дня
Конечный автомат, или FSM, описывает подсистему как набор допустимых состояний, событий и переходов:
state + event + guard -> next_state + actionВажные понятия:
- state - режим подсистемы: WAIT_AT, ONLINE, RECOVERY_BACKOFF.
- event - факт, который уже произошел: AT_OK, TIMEOUT, POWER_WARN.
- guard - условие перехода: совпал ли transaction_id, актуально ли поколение модема.
- action - что надо сделать снаружи автомата: отправить AT, запустить таймер, сбросить EC25.
- entry/exit actions - действия один раз при входе/выходе из состояния.
- invariant - условие, которое обязано быть истинным в состоянии.
Главная мысль: состояние подсистемы должен менять один владелец. ISR, UART parser, CLI и timers не должны напрямую писать s_modem.state; они должны отправлять события в очередь владельца.
2. Зачем это нужно в твоих проектах
Для EC25 состояние быстро превращается в набор несовместимых флагов:
bool modem_ready;
bool network_registered;
bool pdp_active;
bool mqtt_connected;
bool reset_in_progress;Такая модель допускает невозможные комбинации. FSM делает допустимые состояния явными:
OFF
WAIT_BOOT
WAIT_AT
WAIT_REGISTRATION
WAIT_PDP
WAIT_MQTT
ONLINE
RECOVERY_BACKOFF
DEGRADEDДля фаз светофора FSM помогает описать переходы:
UNKNOWN -> CANDIDATE_RED -> STABLE_RED -> CANDIDATE_GREEN -> STABLE_GREEN
UNKNOWN -> CONFLICT
STABLE_* -> INPUT_LOSTДля OTA FSM разделяет разные классы ошибок:
IDLE -> PRECHECK -> MANIFEST -> DOWNLOAD -> VERIFY -> SET_BOOT -> REBOOT_PENDING
PENDING_CONFIRMATION -> CONFIRMED
PENDING_CONFIRMATION -> ROLLBACKОшибка DOWNLOAD оставляет текущую прошивку, а ошибка PENDING_CONFIRMATION должна привести к rollback.
3. Теория
Состояние - это режим поведения
Плохие состояния:
MODEM_HAS_RESPONSE
TIMEOUT_OCCURRED
MQTT_ERRORЭто события или факты, а не режимы. Хорошие состояния:
MODEM_WAIT_BOOT
MODEM_WAIT_AT
MODEM_WAIT_REGISTRATION
MODEM_ONLINE
MODEM_RECOVERY_BACKOFFСостояние отвечает на вопрос: какие события сейчас ожидаются и какие действия разрешены?
Событие - это факт
Хорошие события:
MODEM_EVT_START
MODEM_EVT_AT_OK
MODEM_EVT_AT_ERROR
MODEM_EVT_TIMEOUT
MODEM_EVT_REGISTERED
MODEM_EVT_MQTT_CONNECTED
MODEM_EVT_POWER_WARNПлохие события:
TRY_MQTT
DO_RECOVERY
CHECK_MODEMЭто больше похоже на actions.
FSM-core не должен выполнять side effects
FSM должен возвращать действия, а не дергать UART/GPIO/NVS напрямую:
typedef enum {
MODEM_ACT_NONE = 0,
MODEM_ACT_POWER_ON,
MODEM_ACT_SEND_AT,
MODEM_ACT_QUERY_REGISTRATION,
MODEM_ACT_CONNECT_MQTT,
MODEM_ACT_RESET_HARDWARE,
MODEM_ACT_START_DEADLINE,
MODEM_ACT_REPORT_ONLINE,
} modem_action_type_t;
typedef struct {
modem_action_type_t type;
uint32_t argument;
} modem_action_t;Service layer уже выполняет эти действия через реальные драйверы.
Guards защищают от поздних событий
После reset EC25 старый ответ может прийти поздно. Поэтому событие должно содержать generation/transaction context:
typedef struct {
modem_event_type_t type;
uint32_t transaction_id;
uint32_t modem_generation;
int32_t error;
uint64_t timestamp_us;
} modem_event_t;Событие другого поколения не должно менять новое состояние.
Timeout - обычное событие
Не надо делать vTaskDelay() внутри state transition. При входе в состояние задается deadline, а timer потом отправляет MODEM_EVT_TIMEOUT.
WAIT_AT entry -> SEND_AT + START_DEADLINE(2000 ms)
TIMEOUT event -> RECOVERY_BACKOFFТак FSM продолжает принимать POWER_WARN, STOP, RESET и другие события.
Invariants
Для ONLINE должны быть истинны факты:
powered=true
at_ready=true
registered=true
pdp_active=true
mqtt_connected=trueПроверяй invariants после каждого перехода. В debug это может быть assert, в production - fault event и recovery.
4. Типичные ошибки
- Несколько задач меняют одно состояние напрямую.
- FSM выполняет блокирующую AT-команду.
- Timeout реализован через vTaskDelay().
- state, event и action названы одинаково, например MODEM_RECONNECT.
- Десятки независимых bool создают невозможные состояния.
- Side effect выполняется до фиксации перехода.
- Поздний OK от старой AT-команды завершает новую транзакцию.
- Неизвестные события молча игнорируются.
- Recovery не имеет лимита и backoff.
- Entry action выполняется каждый цикл, отправляя AT сотни раз.
5. Практическое задание на 30-60 минут
Создай STATE_MACHINE_POLICY.md:
# State machine policy
1. Every subsystem state has one owner task.
2. Hardware callbacks only post events.
3. State transitions never block.
4. Timeouts are events based on monotonic deadlines.
5. External side effects are returned as actions.
6. Every transaction has an ID or generation.
7. Invalid transitions are diagnosed.
8. Recovery has retry limits and backoff.
9. State invariants are checked after every transition.
10. FSM core is host-testable without RTOS/HAL.Минимальный список состояний для EC25:
typedef enum {
MODEM_STATE_OFF = 0,
MODEM_STATE_WAIT_BOOT,
MODEM_STATE_WAIT_AT,
MODEM_STATE_WAIT_REGISTRATION,
MODEM_STATE_WAIT_PDP,
MODEM_STATE_WAIT_MQTT,
MODEM_STATE_ONLINE,
MODEM_STATE_RECOVERY_BACKOFF,
MODEM_STATE_DEGRADED,
} modem_state_t;Сделай unit-тесты:
1. Нормальный путь до ONLINE.
2. WAIT_AT timeout -> RECOVERY_BACKOFF.
3. Поздний AT_OK после reset игнорируется по generation.
4. AT_OK с чужим transaction_id не завершает текущую команду.
5. MQTT_DISCONNECTED в ONLINE возвращает WAIT_MQTT, но не reset MCU.
6. Потеря регистрации сбрасывает PDP/MQTT facts.
7. Многократный timeout после лимита переводит DEGRADED.
8. Дубликат MQTT_CONNECTED в ONLINE не создает повторный переход.6. Что почитать или попробовать дальше
- ESP-IDF Event Loop Library: пользовательские event loops, profiling handlers.
- FreeRTOS queues и task notifications.
- CMSIS-RTOS2 Message Queue и Thread Flags.
- Host unit tests для FSM без HAL/RTOS.
Критерии: Проверьте контекст события и правило одного владельца.
Задание
Опишите неблокирующий переход WAIT_AT: что происходит при входе, кто выполняет операцию UART и как timeout попадает в FSM?
Критерии самопроверки: Назовите владельца, возвращаемые actions, side effects service layer и событие timeout. Не помещайте блокирующую работу в FSM-core.
Показать ответ автора
При входе FSM возвращает SEND_AT и START_DEADLINE. Service layer выполняет эти actions. По истечении монотонного deadline таймер отправляет MODEM_EVT_TIMEOUT в очередь владельца; сам переход не вызывает vTaskDelay().