1. Тема дня

Алгоритм детектирования фазы светофора по AC-сигналу после оптопары: окно импульсов, стабильность, подавление ложного OFF и правильный timestamp. Главная задача: Отличать реальное включение/выключение фазы от пульсаций 50/100 Гц, кратких пропаданий, помех и переходных состояний.

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

Если RED реально включён, GPIO может выглядеть так:

text
__--________--________--________--____

Если читать уровень “прямо сейчас”, можно получить ложное:

text
RED -> OFF -> RED -> OFF -> RED

Если это отправлять по UDP/TCP/MQTT, камера или Jetson получат мусорные переключения. Нужно строить алгоритм по наличию импульсов в окне времени.

3. Три уровня состояния

Для каждого входа:

text
raw_active
present
stable

11.3.1 raw_active

Мгновенный уровень GPIO.

11.3.2 present

Сигнал присутствует, если недавно был активный импульс:

text
если активный импульс был за последние N мс:
    present = true
иначе:
    present = false

11.3.3 stable

present не менялся достаточно долго:

text
present изменился
    ждём CONFIRM_TIME
если за это время present не передумал:
    stable = present

Именно stable лучше отдавать в phase_detector.

4. Разные времена

Нужно минимум три времени:

text
AC_PRESENT_TIMEOUT_US
  сколько можно ждать между импульсами и всё ещё считать AC присутствующим
STABLE_ON_US
  сколько present=true должен продержаться, чтобы считать фазу включённой
STABLE_OFF_US
  сколько present=false должен продержаться, чтобы считать фазу выключенной

Часто:

text
STABLE_ON_US  = 20-30 мс
STABLE_OFF_US = 50-100 мс

Выключение подтверждаем осторожнее, чтобы не ловить провал одного-двух импульсов.

5. Timestamp policy

Есть разные времена:

text
t_raw_first       - первый сырой признак изменения
t_present_change  - когда present изменился
t_stable_change   - когда изменение подтверждено
t_event_emit      - когда событие отправили в очередь

Плохой вариант - timestamp в момент отправки по сети. Лучше хранить несколько времён:

c
typedef struct {
    traffic_phase_t old_phase;
    traffic_phase_t new_phase;
    int64_t detected_us;
    int64_t confirmed_us;
    int64_t emitted_us;
    uint32_t raw_mask;
    uint32_t stable_mask;
} phase_event_t;

Тогда можно посчитать задержки:

text
filter_delay = confirmed_us - detected_us
queue_delay  = emitted_us - confirmed_us
network_delay = receive_time - emitted_us

6. AC channel update

Флаг seen_active исключает ложное присутствие на старте. previous_raw_active позволяет считать только переход false→true, наблюдённый polling. Это всё ещё не аппаратный pulse counter: импульс короче периода опроса можно пропустить. last_active_us обновляется на каждом активном sample, чтобы длительный уровень оставался присутствующим. Инициализируйте структуру нулями один раз до цикла и используйте монотонное время одного запуска.

c
typedef struct {
    bool seen_active;
    bool previous_raw_active;
    bool present;
    bool stable;
    uint64_t last_active_us;
    uint64_t present_changed_us;
    uint32_t pulse_count;
} ac_channel_example_t;

#define AC_PRESENT_TIMEOUT_US 45000ULL
#define AC_STABLE_ON_US       25000ULL
#define AC_STABLE_OFF_US      70000ULL

static void ac_channel_example_update(ac_channel_example_t *ch,
                                      bool raw_active,
                                      uint64_t now_us)
{
    if (raw_active) {
        ch->seen_active = true;
        ch->last_active_us = now_us;
        if (!ch->previous_raw_active) ch->pulse_count++;
    }
    ch->previous_raw_active = raw_active;
    bool new_present = ch->seen_active &&
        (now_us - ch->last_active_us <= AC_PRESENT_TIMEOUT_US);
    if (new_present != ch->present) {
        ch->present = new_present;
        ch->present_changed_us = now_us;
    }
    uint64_t required_us = ch->present ? AC_STABLE_ON_US : AC_STABLE_OFF_US;
    if (ch->stable != ch->present &&
        now_us - ch->present_changed_us >= required_us) {
        ch->stable = ch->present;
    }
}

Для OFF используйте отдельную выдержку кандидата 200000 мкс, для обычных фаз — 100000 мкс, а CONFLICT не ждёт этой выдержки. Вызывайте phase_candidate_ready после обновления candidate/candidate_since_us и перед переходом current. Чтобы CONFLICT действительно был немедленным, обновление нового кандидата не должно безусловно делать return до этой проверки. Фильтры AC/stable дают собственную задержку до появления кандидата — она не исчезает от этого helper.

c
#define MIN_PHASE_HOLD_US 100000ULL
#define OFF_REPORT_US     200000ULL

static bool phase_candidate_ready(traffic_phase_t phase,
                                  uint64_t candidate_since_us,
                                  uint64_t now_us)
{
    if (phase == TRAFFIC_PHASE_CONFLICT) return true;
    uint64_t required_us = phase == TRAFFIC_PHASE_OFF
        ? OFF_REPORT_US : MIN_PHASE_HOLD_US;
    return now_us - candidate_since_us >= required_us;
}
  • Старт без активных samples: present=false, pulse_count=0.
  • Десять подряд active samples: pulse_count=1.
  • active→inactive→active: pulse_count=2.
  • OFF-кандидат 199999 мкс не публикуется; на 200000 мкс может публиковаться.
  • CONFLICT проверяется в той же итерации, где появился новый кандидат.
c
typedef struct {
    bool raw_active;
    bool present;
    bool stable;
    int64_t last_raw_active_us;
    int64_t present_changed_us;
    int64_t stable_changed_us;
    int64_t candidate_on_us;
    int64_t candidate_off_us;
    uint32_t pulse_count;
    uint32_t stable_change_count;
    uint32_t short_drop_count;
    uint32_t glitch_count;
} ac_channel_t;
#define AC_PRESENT_TIMEOUT_US   45000
#define AC_STABLE_ON_US         25000
#define AC_STABLE_OFF_US        70000
static void ac_channel_update(ac_channel_t *ch,
                              bool raw_active,
                              int64_t now_us)
{
    ch->raw_active = raw_active;
    if (raw_active) {
        ch->last_raw_active_us = now_us;
        ch->pulse_count++;
    }
    bool new_present =
        (now_us - ch->last_raw_active_us) <= AC_PRESENT_TIMEOUT_US;
    if (new_present != ch->present) {
        ch->present = new_present;
        ch->present_changed_us = now_us;
        if (new_present) {
            ch->candidate_on_us = now_us;
        } else {
            ch->candidate_off_us = now_us;
        }
    }
    if (ch->present != ch->stable) {
        int64_t age_us = now_us - ch->present_changed_us;
        int64_t need_us = ch->present ? AC_STABLE_ON_US : AC_STABLE_OFF_US;
        if (age_us >= need_us) {
            ch->stable = ch->present;
            ch->stable_changed_us = now_us;
            ch->stable_change_count++;
        }
    }
}

7. Декодирование фазы

c
#define PHASE_MASK_RED     (1 << 0)
#define PHASE_MASK_YELLOW  (1 << 1)
#define PHASE_MASK_GREEN   (1 << 2)
static uint32_t make_stable_mask(bool red, bool yellow, bool green)
{
    uint32_t m = 0;
    if (red) m |= PHASE_MASK_RED;
    if (yellow) m |= PHASE_MASK_YELLOW;
    if (green) m |= PHASE_MASK_GREEN;
    return m;
}
typedef enum {
    TRAFFIC_PHASE_UNKNOWN = 0,
    TRAFFIC_PHASE_OFF,
    TRAFFIC_PHASE_RED,
    TRAFFIC_PHASE_YELLOW,
    TRAFFIC_PHASE_GREEN,
    TRAFFIC_PHASE_RED_YELLOW,
    TRAFFIC_PHASE_CONFLICT,
} traffic_phase_t;
static traffic_phase_t decode_phase(uint32_t mask)
{
    switch (mask) {
    case 0:
        return TRAFFIC_PHASE_OFF;
    case PHASE_MASK_RED:
        return TRAFFIC_PHASE_RED;
    case PHASE_MASK_YELLOW:
        return TRAFFIC_PHASE_YELLOW;
    case PHASE_MASK_GREEN:
        return TRAFFIC_PHASE_GREEN;
    case PHASE_MASK_RED | PHASE_MASK_YELLOW:
        return TRAFFIC_PHASE_RED_YELLOW;
    default:
        return TRAFFIC_PHASE_CONFLICT;
    }
}

8. Минимальное время фазы

Светофорные фазы обычно не меняются каждые 100 мс.

c
#define MIN_PHASE_HOLD_US 100000
typedef struct {
    traffic_phase_t current;
    traffic_phase_t candidate;
    int64_t current_since_us;
    int64_t candidate_since_us;
    uint32_t current_mask;
    uint32_t candidate_mask;
    uint32_t phase_change_count;
    uint32_t conflict_count;
} phase_detector_t;

Обновление:

c
static bool phase_detector_update(phase_detector_t *det,
                                  uint32_t stable_mask,
                                  int64_t now_us,
                                  phase_event_t *out)
{
    traffic_phase_t decoded = decode_phase(stable_mask);
    if (decoded != det->candidate) {
        det->candidate = decoded;
        det->candidate_mask = stable_mask;
        det->candidate_since_us = now_us;
        return false;
    }
    bool candidate_old_enough =
        (now_us - det->candidate_since_us) >= MIN_PHASE_HOLD_US;
    bool conflict = (decoded == TRAFFIC_PHASE_CONFLICT);
    if ((candidate_old_enough || conflict) && decoded != det->current) {
        out->old_phase = det->current;
        out->new_phase = decoded;
        out->detected_us = det->candidate_since_us;
        out->confirmed_us = now_us;
        out->emitted_us = now_us;
        out->stable_mask = stable_mask;
        det->current = decoded;
        det->current_mask = stable_mask;
        det->current_since_us = now_us;
        det->phase_change_count++;
        if (conflict) {
            det->conflict_count++;
        }
        return true;
    }
    return false;
}

9. Как избежать ложного OFF между фазами

Реальное переключение:

text
RED выключился
через 50 мс включился GREEN

Наивный алгоритм может выдать:

text
RED -> OFF -> GREEN

Можно ввести правило:

text
OFF считается отдельной фазой только если длится дольше OFF_REPORT_US

Например:

c
#define OFF_REPORT_US 200000

Политика:

text
OFF < 200 мс:
  переходное состояние, не отправлять как отдельную фазу
OFF >= 200 мс:
  отправить PHASE_OFF
CONFLICT:
  отправлять быстро, это аварийная диагностика

10. Рекомендуемая task-схема

text
input_task
  period: 1-5 мс
  reads GPIO raw
  updates ac_channel_update()
  builds stable_mask
  calls phase_detector_update()
  sends phase_event_t to phase_event_queue

Пример:

c
#define INPUT_TASK_PERIOD_MS 2
void input_task(void *arg)
{
    phase_event_t ev;
    while (1) {
        int64_t now_us = esp_timer_get_time();
        bool red_raw = gpio_get_level(RED_GPIO) == RED_ACTIVE_LEVEL;
        bool yellow_raw = gpio_get_level(YELLOW_GPIO) == YELLOW_ACTIVE_LEVEL;
        bool green_raw = gpio_get_level(GREEN_GPIO) == GREEN_ACTIVE_LEVEL;
        ac_channel_update(&s_red, red_raw, now_us);
        ac_channel_update(&s_yellow, yellow_raw, now_us);
        ac_channel_update(&s_green, green_raw, now_us);
        uint32_t stable_mask = make_stable_mask(
            s_red.stable,
            s_yellow.stable,
            s_green.stable
        );
        if (phase_detector_update(&s_phase, stable_mask, now_us, &ev)) {
            ev.emitted_us = esp_timer_get_time();
            if (xQueueSend(phase_event_queue, &ev, 0) != pdTRUE) {
                diag.phase_event_queue_drops++;
            }
        }
        vTaskDelay(pdMS_TO_TICKS(INPUT_TASK_PERIOD_MS));
    }
}

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

  • делать OFF слишком быстрым;
  • отправлять каждую внутреннюю смену present;
  • путать detected_us и confirmed_us;
  • считать RED+GREEN обычным состоянием;
  • логировать каждую итерацию input_task;
  • считать, что GPIO glitch filter решит задачу AC-детектирования.

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

Создай PHASE_DETECTOR_POLICY.md:

markdown
# Phase detector policy
## Input model
Raw GPIO level is not a final phase signal.
For AC optocoupler input:
- raw_active means GPIO currently sees active pulse
- present means pulse was seen recently
- stable means present survived confirmation time
## Timing constants
Initial values:
- INPUT_TASK_PERIOD_MS = 2
- AC_PRESENT_TIMEOUT_US = 45000
- AC_STABLE_ON_US = 25000
- AC_STABLE_OFF_US = 70000
- MIN_PHASE_HOLD_US = 100000
- OFF_REPORT_US = 200000
These values must be validated on oscilloscope/HIL.
## Timestamp policy
phase_event_t contains:
- detected_us
- confirmed_us
- emitted_us
Network send time must not replace physical detection time.
## Phase rules
Valid:
- RED
- YELLOW
- GREEN
- RED_YELLOW
- OFF if long enough
Fault:
- RED + GREEN
- YELLOW + GREEN if not expected
- RED + YELLOW + GREEN

HIL-сценарии:

text
1. RED pulse train 50/100 Hz for 3 s
   expected: OFF/UNKNOWN -> RED
2. RED pulse train stops for 30 ms and resumes
   expected: no RED -> OFF event
3. RED stops, GREEN starts after 80 ms
   expected: RED -> GREEN or RED -> TRANSITION -> GREEN
4. RED and GREEN pulse together for 300 ms
   expected: CONFLICT event
5. Single YELLOW glitch 5 ms during RED
   expected: no YELLOW phase event
6. No pulses for 1 s
   expected: OFF

13. Короткий итог

Для 220 В AC после оптопары фаза определяется не мгновенным уровнем GPIO, а наличием импульсов в окне времени и подтверждением стабильности. Правильная цепочка:

text
raw GPIO
  -> raw_active
  -> present: импульс был недавно
  -> stable: состояние подтверждено
  -> stable_mask
  -> decode_phase()
  -> phase_event_t
  -> transport_task

Стартовые значения:

text
INPUT_TASK_PERIOD_MS   = 2 мс
AC_PRESENT_TIMEOUT_US  = 45 мс
AC_STABLE_ON_US        = 25 мс
AC_STABLE_OFF_US       = 70 мс
MIN_PHASE_HOLD_US      = 100 мс
OFF_REPORT_US          = 200 мс

Главное: Timestamp изменения фазы должен появляться до сети, модема, MQTT/TCP/UDP и логов.

Raw GPIO неактивен между AC pulses, но pulse был недавно и подтверждённый state остаётся on. Что передавать phase_detector по уроку?

Задание

Event имеет detected_us=12000, confirmed_us=92000, emitted_us=97000. Вычислите filter_delay и queue_delay по формулам урока. Почему замена detected_us временем network send теряет информацию?

Критерии самопроверки: Получите 80 ms и 5 ms, сохранив все три timestamps вместо network send time.

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

filter_delay=92000-12000=80000 µs=80 ms. queue_delay=97000-92000=5000 µs=5 ms. Detection, confirmation и emission описывают разные стадии; network-send timestamp скрывает физическое detection time и мешает отделить filter delay от queue/network delay.