1. Тема дня

Машина состояний для Quectel EC25: AT-команды, OK, ERROR, URC, таймауты, MQTT/TCP/UDP reconnect и восстановление связи. Главная мысль: Модем нельзя обслуживать как последовательность send_at(); delay();. Модем должен быть отдельным сервисом со своим состоянием, очередью запросов, таймаутами и восстановлением.

2. Зачем это нужно

Типичные проблемы EC25:

  • AT иногда отвечает сразу, иногда нет;
  • модем может быть уже включён после старта ESP32;
  • QMTOPEN/QMTCONN могут зависать или возвращать ошибки;
  • URC может прийти в любой момент;
  • сеть может пропасть после успешного подключения;
  • MQTT может отвалиться;
  • нельзя блокировать быстрый путь детектирования фазы.

Плохой подход:

c
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

Модем всегда находится в одном из состояний:

text
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

Переходы происходят по событиям:

text
AT_OK
AT_ERROR
URC_QMTOPEN
URC_QMTCONN
URC_QMTSTAT
TIMEOUT
NETWORK_LOST
REQUEST_RECONNECT
POWERKEY_DONE
RESET_DONE

4. AT-команды не похожи на обычные функции

Обычная функция возвращает результат сразу. AT-команда может иметь несколько фаз:

text
1. ESP32 отправил строку AT-команды.
2. Модем сразу ответил OK или ERROR.
3. Позже пришёл URC с результатом операции.

Для MQTT:

text
AT+QMTOPEN=...
OK
+QMTOPEN: 0,0
AT+QMTCONN=...
OK
+QMTCONN: 0,0,0

OK часто означает только “команда принята”, а не “операция завершена успешно”.

5. Архитектура modem_service

text
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. Структура состояния

c
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. Переходы без блокирующего ожидания

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

Задача:

c
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 в события:

text
OK
ERROR
+CME ERROR: 10
+QMTOPEN: 0,0
+QMTOPEN: 0,5
+QMTCONN: 0,0,0
+QMTSTAT: 0,3

Событие:

c
typedef struct {
    modem_event_type_t type;
    int client_idx;
    int result;
    int ret_code;
    int raw_error;
} modem_event_t;

Парсер не решает “перезапустить модем”. Он только распознаёт событие.

9. Очередь запросов к модему

c
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

Плохая стратегия:

text
любая ошибка -> reset ESP32

Правильная лестница восстановления:

text
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:

markdown
# 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(), а отдельным сервисом:

text
transport_task
  -> modem_request_queue
  -> modem_task
      -> UART2
      -> AT parser
      -> modem state machine
      -> recovery ladder
AT+QMTOPEN вернул OK, но финальный +QMTOPEN URC ещё не пришёл. Что означает OK в модели урока?

Задание

Во время открытия 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.