1. Тема дня
Машина состояний для Quectel EC25: AT-команды, OK, ERROR, URC, таймауты, MQTT/TCP/UDP reconnect и восстановление связи. Главная мысль: Модем нельзя обслуживать как последовательность send_at(); delay();. Модем должен быть отдельным сервисом со своим состоянием, очередью запросов, таймаутами и восстановлением.
2. Зачем это нужно
Типичные проблемы EC25:
- AT иногда отвечает сразу, иногда нет;
- модем может быть уже включён после старта ESP32;
- QMTOPEN/QMTCONN могут зависать или возвращать ошибки;
- URC может прийти в любой момент;
- сеть может пропасть после успешного подключения;
- MQTT может отвалиться;
- нельзя блокировать быстрый путь детектирования фазы.
Плохой подход:
send_at("AT+QMTOPEN=0,\"broker\",1883\r\n");
wait_until_response();
send_at("AT+QMTCONN=0,\"client\"\r\n");
wait_until_response();3. Что такое state machine
Модем всегда находится в одном из состояний:
MODEM_OFF
MODEM_POWERING_ON
MODEM_AT_SYNC
MODEM_SIM_CHECK
MODEM_REG_WAIT
MODEM_PDP_CONFIG
MODEM_PDP_ACTIVE
MODEM_MQTT_CONFIG
MODEM_MQTT_OPENING
MODEM_MQTT_CONNECTING
MODEM_READY
MODEM_DEGRADED
MODEM_RECOVERING
MODEM_ERRORПереходы происходят по событиям:
AT_OK
AT_ERROR
URC_QMTOPEN
URC_QMTCONN
URC_QMTSTAT
TIMEOUT
NETWORK_LOST
REQUEST_RECONNECT
POWERKEY_DONE
RESET_DONE4. AT-команды не похожи на обычные функции
Обычная функция возвращает результат сразу. AT-команда может иметь несколько фаз:
1. ESP32 отправил строку AT-команды.
2. Модем сразу ответил OK или ERROR.
3. Позже пришёл URC с результатом операции.Для MQTT:
AT+QMTOPEN=...
OK
+QMTOPEN: 0,0
AT+QMTCONN=...
OK
+QMTCONN: 0,0,0OK часто означает только “команда принята”, а не “операция завершена успешно”.
5. Архитектура modem_service
transport_task
-> modem_request_queue
modem_task
├─ owns UART2
├─ owns AT parser
├─ owns modem state
├─ owns command timeout
├─ owns reconnect policy
└─ produces modem events/statusПравило: UART2 принадлежит только modem_task. CLI, MQTT, SMS, transport не должны напрямую писать AT-команды в UART2.
6. Структура состояния
typedef enum {
MODEM_STATE_OFF = 0,
MODEM_STATE_POWERING_ON,
MODEM_STATE_AT_SYNC,
MODEM_STATE_ECHO_OFF,
MODEM_STATE_SIM_CHECK,
MODEM_STATE_REG_WAIT,
MODEM_STATE_PDP_CONFIG,
MODEM_STATE_PDP_ACTIVATE,
MODEM_STATE_MQTT_CONFIG,
MODEM_STATE_MQTT_OPENING,
MODEM_STATE_MQTT_CONNECTING,
MODEM_STATE_READY,
MODEM_STATE_RECOVERING,
MODEM_STATE_ERROR,
} modem_state_t;
typedef struct {
modem_state_t state;
int64_t state_entered_us;
int64_t deadline_us;
uint8_t client_idx;
bool at_ready;
bool sim_ready;
bool registered_network;
bool pdp_active;
bool mqtt_open;
bool mqtt_connected;
uint32_t at_timeout_count;
uint32_t mqtt_open_fail_count;
uint32_t mqtt_connect_fail_count;
uint32_t recover_count;
} modem_ctx_t;Состояние должно отвечать:
- где мы сейчас;
- когда вошли;
- какой deadline;
- какую команду ждём;
- какой последний результат;
- сколько попыток восстановления.
7. Переходы без блокирующего ожидания
static void modem_enter_state(modem_ctx_t *m,
modem_state_t new_state,
int timeout_ms)
{
m->state = new_state;
m->state_entered_us = esp_timer_get_time();
m->deadline_us = m->state_entered_us + timeout_ms * 1000LL;
}Задача:
void modem_task(void *arg)
{
modem_ctx_t m = {0};
modem_enter_state(&m, MODEM_STATE_AT_SYNC, 1000);
while (1) {
modem_poll_uart_events(&m);
modem_process_requests(&m);
modem_step(&m);
vTaskDelay(pdMS_TO_TICKS(10));
}
}8. Парсер отдельно от state machine
Парсер превращает строки UART в события:
OK
ERROR
+CME ERROR: 10
+QMTOPEN: 0,0
+QMTOPEN: 0,5
+QMTCONN: 0,0,0
+QMTSTAT: 0,3Событие:
typedef struct {
modem_event_type_t type;
int client_idx;
int result;
int ret_code;
int raw_error;
} modem_event_t;Парсер не решает “перезапустить модем”. Он только распознаёт событие.
9. Очередь запросов к модему
typedef enum {
MODEM_REQ_MQTT_PUBLISH,
MODEM_REQ_UDP_SEND,
MODEM_REQ_TCP_SEND,
MODEM_REQ_RECONNECT,
MODEM_REQ_GET_STATUS,
} modem_req_type_t;
typedef struct {
modem_req_type_t type;
uint8_t priority;
uint32_t seq;
int64_t created_at_us;
char topic[96];
uint8_t payload[256];
uint16_t payload_len;
} modem_request_t;Внешний код отправляет запрос, а modem_task забирает его только в подходящем состоянии.
10. Recovery ladder
Плохая стратегия:
любая ошибка -> reset ESP32Правильная лестница восстановления:
1. Повторить текущую AT-команду.
2. Закрыть MQTT-сессию: QMTDISC/QMTCLOSE.
3. Переоткрыть MQTT: QMTOPEN/QMTCONN.
4. Переактивировать PDP.
5. Проверить регистрацию сети.
6. Перезапустить модем через PWRKEY/RESET.
7. Перезагрузить ESP32 только если прошивка сама перестала быть здоровой.11. Типичные ошибки
- считать OK финальным результатом;
- использовать vTaskDelay(5000) вместо ожидания конкретного события;
- несколько владельцев UART2;
- игнорировать URC;
- использовать один глобальный modem_ready;
- не различать телеметрию и критичные события.
12. Практическое задание
Создай MODEM_STATE_MACHINE.md:
# EC25 modem state machine
| State | Entry action | Expected event | Timeout | On success | On error/timeout |
|---|---|---|---:|---|---|
| AT_SYNC | send `AT` | `OK` | 1000 ms | ECHO_OFF | POWERING_ON |
| ECHO_OFF | send `ATE0` | `OK` | 1000 ms | SIM_CHECK | AT_SYNC |
| SIM_CHECK | send `AT+CPIN?` | `READY` | 5000 ms | REG_WAIT | RECOVERING |
| REG_WAIT | query registration | registered | 60000 ms | PDP_CONFIG | RECOVERING |
| PDP_CONFIG | configure APN/PDP | `OK` | 5000 ms | PDP_ACTIVATE | RECOVERING |
| PDP_ACTIVATE | activate PDP | active | 60000 ms | MQTT_CONFIG | RECOVERING |
| MQTT_OPENING | `QMTOPEN` | `+QMTOPEN: 0,0` | 60000 ms | MQTT_CONNECTING | MQTT/PDP recovery |
| MQTT_CONNECTING | `QMTCONN` | `+QMTCONN: 0,0,...` | 30000 ms | READY | MQTT recovery |
| READY | process request queue | request / URC | - | READY | RECOVERING |13. Короткий итог
EC25 должен быть не набором блокирующих send_at(), а отдельным сервисом:
transport_task
-> modem_request_queue
-> modem_task
-> UART2
-> AT parser
-> modem state machine
-> recovery ladderЗадание
Во время открытия MQTT пришёл network-loss URC. Какие слои распознают строку и выбирают recovery? Почему CLI не должна отправлять AT command прямо в UART2?
Критерии самопроверки: Разделите parsing и state/recovery; сохраните одного owner UART2 и request queue.
Показать ответ автора
UART/parser распознаёт строку и создаёт event. modem_task владеет state, deadlines, порядком requests и recovery, поэтому решает, как обработать network loss. CLI отправляет request в queue; прямая запись UART2 создаёт второго owner и может перемешать commands и responses.