1. Тема дня

Идея: записывать не произвольный лог, а входные события de- terministic core, чтобы потом точно воспроизвести поведение FSM на ПК.

text
REAL DEVICE:
GPIO / ADC decisions / UART EC25 / MQTT / timers
    -> normalized events
    -> production FSM + event recorder
    -> replay file
LINUX HOST:
replay file
    -> same FSM/core
    -> deterministic result

Главная мысль: не нужно эмулировать весь ESP32, EC25 и интернет; нужно повторно подать в чистую логику ту же последовательность значимых событий.

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

Редкая полевая ошибка EC25 может зависеть от порядка:

text
+CEREG:0
MQTT socket lost
AT timeout
+CEREG:1
late OK

Обычный лог не всегда позволяет восстановить порядок. Replay- файл позволяет запустить тот же сценарий на ПК и превратить ошибку в regression test.

3. Теория

Что записывать

Записываем inputs в core:

text
MODEM_LINE
MODEM_URC
MQTT_CONNECTED
MQTT_DISCONNECTED
COMMAND_RECEIVED
GPIO_EDGE
ADC_DECISION
CAN_FRAME
TIMER_EXPIRED
POWER_STATE_CHANGED
CONFIG_LOADED

Не нужно записывать внутренности RTOS: task switch, mutex taken, malloc, printf.

Timer — тоже событие

FSM не должна спрашивать реальный esp_timer_get_time(). Timer service генерирует TIMER_EXPIRED. Replay просто подаёт записанный event.

Logical time

Event содержит mono_us, но replay не должен ждать реальные 40 секунд. Он может мгновенно установить logical clock на times- tamp события.

Sequence важнее timestamp

Timestamp может совпасть у нескольких событий. Основной порядок задаёт sequence. Fragmentation сохраняется

Если тестируется parser, нужно сохранить chunk boundaries:

text
callback 17 bytes
callback 31 bytes
callback 4 bytes

Иначе parser bug может исчезнуть.

Outputs как assertions

Можно записывать expected actions:

text
INPUT: CEREG_REGISTERED
OUTPUT: SEND_AT QIACT

Replay подаёт stimuli, а observations использует как assertions.

State hash

После каждого события можно считать hash canonical state pro- jection, не сырой C-структуры.

4. Типичные ошибки

  1. Записывать текст вместо событий.
  2. Записывать только ошибки, а не pre-trigger историю.
  3. Не сохранять sequence.
  4. Использовать real time в replay.
  5. FSM сама читает UART/GPIO/timer.
  6. FSM сама делает MQTT publish.
  7. На ПК использовать Python-модель вместо production C core.
  8. Не сохранять UART/TCP fragmentation при parser replay.
  9. Replay raw ADC, когда нужен только FSM replay.
  10. Не version’ировать event schema.
  11. Хешировать сырые structs.
  12. Не проверять invariants после каждого события.
  13. Recorder сам ломает timing.
  14. Писать каждое событие сразу во Flash.
  15. Записывать секреты в replay artifact.

5. Практическое задание

Сделай replay для modem_core с событиями:

c
typedef enum {
    MODEM_EV_AT_OK = 1,
    MODEM_EV_AT_ERROR,
    MODEM_EV_CEREG_CHANGED,
    MODEM_EV_TIMER_EXPIRED,
} modem_event_type_t;
typedef struct {
    uint32_t sequence;
    uint64_t mono_us;
    modem_event_type_t type;
    int32_t argument;
} modem_replay_event_t;

Сначала используй текстовый replay v0:

text
1 0 CEREG 1
2 1000 TIMER 10
3 1100 OK 0
4 1200 CEREG 0
5 4200 TIMER 11
6 4300 CEREG 1
7 4400 OK 0

После каждого event выводи state dump и проверяй invariants. Сделай сценарий late OK: timeout старой transaction, новая trans- action, потом AT_OK старой transaction. Если текущая модель не различает transaction ID, добавь его.

6. Что попробовать дальше

  • Записывать .evlog на Raspberry Pi через diagnostic UART.
  • Конвертировать field replay в fuzz seed.
  • Добавить differential replay: старый modem FSM vs новый mo- dem FSM после refactoring.
Два input events имеют одинаковый timestamp mono_us. Что определяет порядок replay в уроке?

Задание

Parser получил callbacks на 17, 31 и 4 bytes. Почему replay одним объединённым callback на 52 bytes может не воспроизвести сбой? Назовите две проверки для точного replay.

Критерии самопроверки: Объясните зависимость от fragmentation и сохраните порядок плюс output/invariant checks.

Показать ответ автора

Объединение меняет fragmentation и может убрать bug на границе chunks. Сохраните callbacks 17/31/4 bytes и sequence событий; после каждого event сравнивайте ожидаемые actions и проверяйте invariants в том же core. Logical time может сразу переходить к записанному timestamp.