1. Today's topic
An algorithm for detecting a traffic-light phase from an AC signal after an optocoupler: a pulse window, stability, suppression of false OFF events and correct timestamps. The main task: distinguish genuine phase switching from 50/100 Hz pulsation, brief dropouts, interference and transitional states.
2. Why this matters
When RED is actually on, GPIO may look like this:
__--________--________--________--____Reading the level “right now” may produce a false sequence:
RED -> OFF -> RED -> OFF -> REDIf this is sent over UDP/TCP/MQTT, the camera or Jetson receives spurious transitions. The algorithm must detect pulses within a time window.
3. Three levels of state
For each input:
raw_active
present
stable11.3.1 raw_active
The instantaneous GPIO level.
11.3.2 present
The signal is present if an active pulse occurred recently:
if an active pulse occurred within the last N ms:
present = true
else:
present = false11.3.3 stable
present has remained unchanged long enough:
present changes
wait for CONFIRM_TIME
if present has not changed again during this time:
stable = presentstable is the appropriate value to pass to phase_detector.
4. Different time intervals
At least three intervals are needed:
AC_PRESENT_TIMEOUT_US
how long to wait between pulses and still consider AC present
STABLE_ON_US
how long present=true must hold before considering the phase on
STABLE_OFF_US
how long present=false must hold before considering the phase offOften:
STABLE_ON_US = 20-30 ms
STABLE_OFF_US = 50-100 msConfirm switching off more cautiously to avoid reacting to one or two missing pulses.
5. Timestamp policy
There are different timestamps:
t_raw_first - the first raw indication of a change
t_present_change - when present changes
t_stable_change - when the change is confirmed
t_event_emit - when the event is sent to the queueA poor choice is to timestamp the network send. It is better to retain several times:
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;Then delays can be calculated:
filter_delay = confirmed_us - detected_us
queue_delay = emitted_us - confirmed_us
network_delay = receive_time - emitted_us6. AC channel update
seen_active prevents false presence at startup. previous_raw_active counts only false-to-true transitions observed by polling. This remains a sampled edge counter, not a hardware pulse counter: polling may miss a pulse shorter than its period. Refresh last_active_us for every active sample so a sustained active level remains present. Zero-initialize the structure once before the loop and use monotonic time from one boot.
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;
}
}Require an OFF candidate to last 200000 us, an ordinary phase candidate 100000 us, and do not add this hold delay to CONFLICT. Call phase_candidate_ready after updating candidate/candidate_since_us and before committing current. To report CONFLICT immediately, changing the candidate must not unconditionally return before this check. The AC and stable filters add their own delay before the candidate exists; this helper does not remove it.
#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;
}- Boot without active samples: present=false, pulse_count=0.
- Ten consecutive active samples: pulse_count=1.
- active→inactive→active: pulse_count=2.
- An OFF candidate at 199999 us is not committed; at 200000 us it may be committed.
- Check CONFLICT in the same iteration in which the new candidate appears.
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. Phase decoding
#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. Minimum phase duration
Traffic-light phases do not normally change every 100 ms.
#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;Update:
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. Avoiding a false OFF between phases
A real transition:
RED switches off
GREEN switches on 50 ms laterA naive algorithm may report:
RED -> OFF -> GREENA rule can be introduced:
OFF is considered a separate phase only if it lasts longer than OFF_REPORT_USFor example:
#define OFF_REPORT_US 200000Policy:
OFF < 200 ms:
transitional state, do not send it as a separate phase
OFF >= 200 ms:
send PHASE_OFF
CONFLICT:
send promptly: this is fault diagnostics10. Recommended task structure
input_task
period: 1-5 ms
reads GPIO raw
updates ac_channel_update()
builds stable_mask
calls phase_detector_update()
sends phase_event_t to phase_event_queueAn example:
#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. Common mistakes
- Reporting OFF too quickly;
- sending every internal present transition;
- confusing detected_us with confirmed_us;
- treating RED+GREEN as a normal state;
- logging every input_task iteration;
- assuming a GPIO glitch filter solves AC detection.
12. Practical task
Create PHASE_DETECTOR_POLICY.md:
# 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 + GREENHIL scenarios:
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: OFF13. A short recap
For 220 V AC after an optocoupler, the phase is determined by pulse presence within a time window and confirmation of stability, not by the instantaneous GPIO level. The correct chain:
raw GPIO
-> raw_active
-> present: a pulse occurred recently
-> stable: state has been confirmed
-> stable_mask
-> decode_phase()
-> phase_event_t
-> transport_taskInitial values:
INPUT_TASK_PERIOD_MS = 2 ms
AC_PRESENT_TIMEOUT_US = 45 ms
AC_STABLE_ON_US = 25 ms
AC_STABLE_OFF_US = 70 ms
MIN_PHASE_HOLD_US = 100 ms
OFF_REPORT_US = 200 msThe key point: the phase-change timestamp must be captured before the network, modem, MQTT/TCP/UDP and logs.
Exercise
A recorded event has detected_us=12000, confirmed_us=92000 and emitted_us=97000. Calculate filter_delay and queue_delay using the lesson’s formulas. Explain why replacing detected_us with the network-send time loses information.
Self-check criteria: Calculate 80 ms and 5 ms, retaining all three timestamps rather than substituting network send time.
Show the supplied answer
filter_delay=92000-12000=80000 µs=80 ms. queue_delay=97000-92000=5000 µs=5 ms. Detection, confirmation and emission describe different stages; a network-send timestamp hides the physical detection time and prevents separating filter delay from queue/network delay.