1. Тема дня

Разбираем обработку отказов и безопасные деградированные режимы:

text
transient fault
recoverable fault
degraded mode
fatal fault
fault domain
recovery policy
fault snapshot
supervisor

Главная мысль: хорошая прошивка не должна реагировать на любую ошибку одинаково. Иногда надо повторить операцию, иногда отключить одну подсистему, иногда перейти в degraded mode, а иногда действительно перезагрузиться.

2. Зачем это нужно в проектах

В твоих ESP32/STM32-проектах много подсистем, которые могут ломаться независимо:

text
EC25:
  модем не отвечает на AT;
  PDP context не поднимается;
  MQTT отвалился;
  TCP socket завис.
I2C:
  ADS1115 не отвечает;
  GPIO-expander дал NACK;
  шина зависла из-за SDA low.
Светофорные входы:
  один канал шумит;
  RED и GREEN одновременно активны;
  ADC diagnostic не совпадает с GPIO state.
CAN/RS-485:
  bus-off;
  CRC errors;
  frame timeout;
  чужой трафик забил шину.

Если на всё отвечать reboot(), устройство будет казаться ненадёжным, хотя часто проблема локальная: например, умер ADS1115, но fast path фаз через GPIO всё ещё работает.

3. Теория

Классы ошибок

c
typedef enum {
    FAULT_CLASS_TRANSIENT = 0,
    FAULT_CLASS_RECOVERABLE,
    FAULT_CLASS_DEGRADED,
    FAULT_CLASS_FATAL,
} fault_class_t;

Transient fault - кратковременная ошибка, которая может пройти сама: один I2C NACK, один MQTT publish timeout, единичная UART framing error, короткий пропуск AC-импульса. Реакция: счётчик и rate-limited warning, без reset. Recoverable fault - ошибка повторилась, но подсистему можно восстановить: EC25 не отвечает 30 секунд, I2C device не отвечает несколько попыток, UART DMA завис, CAN ушёл в bus-off впервые. Реакция: локальная recovery state machine. Degraded fault - часть системы недоступна, но устройство может выполнять главную функцию: ADS1115 offline, MQTT недоступен, EC25 offline, CAN offline, logger overload. Реакция: пометить подсистему offline, продолжить fast path, периодически пробовать восстановить. Fatal fault - продолжать нельзя или опасно: heap corruption, stack overflow, повреждение критической конфигурации, input_task не работает и recovery не помогла, brownout. Реакция: safe outputs, fault snapshot, reset/halt.

Fault domains

Fault domain - область, внутри которой ошибка сначала лечится локально.

text
INPUT_DOMAIN:
  GPIO input driver
  phase_detector
  input diagnostics
MODEM_DOMAIN:
  UART EC25
  AT parser
  PDP/MQTT/TCP state machine
I2C_DOMAIN:
  ADS1115
  GPIO expanders
  I2C manager
CAN_DOMAIN:
  CAN driver
  filters
  TX/RX queues

Ошибка внутри домена сначала лечится внутри домена. Только если recovery не помогла, fault поднимается выше - к supervisor.

Degraded mode

Degraded mode должен быть явным состоянием:

c
typedef enum {
    SYSTEM_MODE_NORMAL = 0,
    SYSTEM_MODE_DEGRADED,
    SYSTEM_MODE_RECOVERY,
    SYSTEM_MODE_FATAL,
} system_mode_t;

Примеры:

text
MODE_DEGRADED_NO_MODEM:
  локально детектируем фазы, но не отправляем через EC25/MQTT.
MODE_DEGRADED_NO_ADC_DIAG:
  fast phase detector работает, но analog confidence unavailable.
MODE_DEGRADED_INPUT_CONFLICT:
  входы читаются, но текущая фаза помечена conflict/unknown.

Degraded mode должен сохранять максимально возможную полезную функцию и явно сообщать, что именно недоступно.

Fault event

c
typedef enum {
    FAULT_SRC_SYSTEM = 1,
    FAULT_SRC_INPUT,
    FAULT_SRC_PHASE,
    FAULT_SRC_MODEM,
    FAULT_SRC_MQTT,
    FAULT_SRC_I2C,
    FAULT_SRC_ADS1115,
    FAULT_SRC_GPIO_EXPANDER,
    FAULT_SRC_UART,
    FAULT_SRC_CAN,
    FAULT_SRC_RS485,
    FAULT_SRC_POWER,
} fault_source_t;
typedef enum {
    FAULT_ACTION_NONE = 0,
    FAULT_ACTION_RETRY,
    FAULT_ACTION_REINIT_DRIVER,
    FAULT_ACTION_RESET_DEVICE,
    FAULT_ACTION_MARK_OFFLINE,
    FAULT_ACTION_ENTER_DEGRADED,
    FAULT_ACTION_SYSTEM_RESET,
} fault_action_t;
typedef struct {
    uint32_t seq;
    int64_t detected_us;
    fault_source_t source;
    fault_class_t fault_class;
    fault_action_t action;
    int32_t error_code;
    uint32_t arg0;
    uint32_t arg1;
    uint32_t arg2;
    uint32_t repeat_count;
} fault_event_t;

Такой event можно положить в ring buffer, показать через CLI, отправить по MQTT/CAN или сохранить в fault snapshot.

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

  • Использовать ESP_ERROR_CHECK() на recoverable ошибках.
  • Любую ошибку лечить reboot.
  • Делать recovery без backoff и retry limit.
  • Не различать “главная функция недоступна” и “диагностика недоступна”.
  • Делать recovery внутри ISR/callback.
  • Не показывать degraded mode наружу через CLI/MQTT/CAN.
  • Не тестировать fault path в HIL.

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

Создай FAULT_HANDLING_POLICY.md.

markdown
# Fault handling policy
## Fault classes
| Class | Meaning | Action |
|---|---|---|
| TRANSIENT | single temporary error | counter + rate-limited log |
| RECOVERABLE | repeated/local error | local recovery |
| DEGRADED | subsystem unavailable, main function partly works | mark offline, continue, report |
| FATAL | unsafe or main function impossible | safe outputs, snapshot, reset/halt |

Затем опиши domains, fault codes и CLI-команду faults. Пример CLI:

text
faults:
SYSTEM_MODE=DEGRADED
degraded_mask=NO_MODEM|NO_ADC_DIAG
recent:
123456.100 I2C ADS1115_OFFLINE class=DEGRADED action=MARK_OFFLINE count=3
123500.250 MODEM NO_RESPONSE class=RECOVERABLE action=RESET_DEVICE count=1
123900.800 PHASE CONFLICT class=TRANSIENT action=NONE count=2

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

  • Реализовать fault_report() и fault_manager_task.
  • Добавить HIL-сценарии: ADS1115 unplug, EC25 no response, RED+GREEN conflict, input_task freeze, CAN bus-off.
  • Проверить, что reboot вызывается только для FATAL, а не для любого NACK/timeout.

Задание

ADS1115 перестал отвечать, но GPIO phase detector работает. Определите fault domain, degraded mode, лимит локального восстановления и внешний статус.

Критерии самопроверки: Отделить потерю диагностики от потери главной функции; назвать retry limit/backoff и наблюдаемый статус. Безусловный reboot не удовлетворяет заданию.

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

Сохранить GPIO fast path, пометить диагностику ADC offline, сообщить деградированный статус и повторять попытки по локальной политике I2C. Передавать отказ выше только после исчерпания ограниченной политики или при опасности для главной функции.

Задание

Спроектируйте HIL-проверку input_task freeze. Укажите внедряемый отказ, ожидаемое решение supervisor и доказательство перехода к safe outputs.

Критерии самопроверки: Записать timeout прогресса и результат recovery, а не только счётчик reboot; объяснить, как проверка доказывает переход к безопасным выходам.

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

Остановить прогресс input_task в контролируемом низковольтном тесте, наблюдать health/fault policy и ограниченное восстановление, затем проверить safe outputs и fault snapshot при переходе по fatal escalation path.