← Back to Main Page

Traffic Light Controller

Traffic light controller project with pedestrian crossing support, countdown system, and state machine implementation.

Video Presentation

Open Video

Source Code

Download crossroad.ino

// ================= PINS =================
const int NS_RED    = 23;
const int NS_YELLOW = 22;
const int NS_GREEN  = 21;

const int EW_RED    = 19;
const int EW_YELLOW = 18;
const int EW_GREEN  = 5;

const int PED_BTN_1 = 32;
const int PED_BTN_2 = 33;


const int LED_ON  = LOW;
const int LED_OFF = HIGH;


const unsigned long GREEN_TIME   = 15000; // 15 sec
const unsigned long YELLOW_TIME  = 3000;  // 3 sec
const unsigned long PED_WAIT     = 3000;  // 3 sec after button press
const unsigned long PED_RED_TIME = 10000; // 10 sec both red
const unsigned long DEBOUNCE_MS  = 50;

// ================= STATES =================
enum State {
  NS_GREEN_STATE,
  NS_YELLOW_STATE,
  EW_GREEN_STATE,
  EW_YELLOW_STATE,
  PED_BOTH_YELLOW_STATE,
  PED_BOTH_RED_STATE
};

State currentState = NS_GREEN_STATE;
State resumeState  = NS_GREEN_STATE;

unsigned long stateStartTime = 0;
long lastCountdownSecond = -1;

// pedestrian request
bool pedWaiting = false;
unsigned long pedRequestTime = 0;
long lastPedCountdownSecond = -1;

// debounce vars
bool lastReading1 = HIGH;
bool stableReading1 = HIGH;
unsigned long lastDebounce1 = 0;

bool lastReading2 = HIGH;
bool stableReading2 = HIGH;
unsigned long lastDebounce2 = 0;

// ================= LIGHT FUNCTIONS =================
void allLightsOff() {
  digitalWrite(NS_RED, LED_OFF);
  digitalWrite(NS_YELLOW, LED_OFF);
  digitalWrite(NS_GREEN, LED_OFF);

  digitalWrite(EW_RED, LED_OFF);
  digitalWrite(EW_YELLOW, LED_OFF);
  digitalWrite(EW_GREEN, LED_OFF);
}

void setLights(int nsR, int nsY, int nsG, int ewR, int ewY, int ewG) {
  digitalWrite(NS_RED, nsR);
  digitalWrite(NS_YELLOW, nsY);
  digitalWrite(NS_GREEN, nsG);

  digitalWrite(EW_RED, ewR);
  digitalWrite(EW_YELLOW, ewY);
  digitalWrite(EW_GREEN, ewG);
}

const char* stateName(State state) {
  switch (state) {
    case NS_GREEN_STATE:
      return "NS GREEN, EW RED";
    case NS_YELLOW_STATE:
      return "NS YELLOW, EW RED";
    case EW_GREEN_STATE:
      return "EW GREEN, NS RED";
    case EW_YELLOW_STATE:
      return "EW YELLOW, NS RED";
    case PED_BOTH_YELLOW_STATE:
      return "BOTH YELLOW";
    case PED_BOTH_RED_STATE:
      return "BOTH RED";
  }

  return "UNKNOWN";
}

unsigned long stateDuration(State state) {
  switch (state) {
    case NS_GREEN_STATE:
    case EW_GREEN_STATE:
      return GREEN_TIME;
    case NS_YELLOW_STATE:
    case EW_YELLOW_STATE:
    case PED_BOTH_YELLOW_STATE:
      return YELLOW_TIME;
    case PED_BOTH_RED_STATE:
      return PED_RED_TIME;
  }

  return 0;
}

void logStateCountdown(unsigned long now) {
  unsigned long duration = stateDuration(currentState);

  if (duration == 0) {
    return;
  }

  unsigned long elapsed = now - stateStartTime;
  unsigned long remaining = (elapsed >= duration) ? 0 : duration - elapsed;
  long remainingSeconds = (remaining + 999) / 1000;

  if (remainingSeconds != lastCountdownSecond) {
    lastCountdownSecond = remainingSeconds;
    Serial.print("TIME BEFORE CHANGE (");
    Serial.print(stateName(currentState));
    Serial.print("): ");
    Serial.print(remainingSeconds);
    Serial.println(" sec");
  }
}

void logPedestrianCountdown(unsigned long now) {
  if (!pedWaiting) {
    return;
  }

  unsigned long elapsed = now - pedRequestTime;
  unsigned long remaining = (elapsed >= PED_WAIT) ? 0 : PED_WAIT - elapsed;
  long remainingSeconds = (remaining + 999) / 1000;

  if (remainingSeconds != lastPedCountdownSecond) {
    lastPedCountdownSecond = remainingSeconds;
    Serial.print("PEDESTRIAN: both yellow starts in ");
    Serial.print(remainingSeconds);
    Serial.println(" sec");
  }
}

void changeState(State newState) {
  currentState = newState;
  stateStartTime = millis();
  lastCountdownSecond = -1;

  switch (currentState) {
    case NS_GREEN_STATE:
      // NS green, EW red
      setLights(LED_OFF, LED_OFF, LED_ON,
                LED_ON,  LED_OFF, LED_OFF);
      Serial.print("STATE: ");
      Serial.println(stateName(currentState));
      break;

    case NS_YELLOW_STATE:
      // NS yellow, EW red
      setLights(LED_OFF, LED_ON,  LED_OFF,
                LED_ON,  LED_OFF, LED_OFF);
      Serial.print("STATE: ");
      Serial.println(stateName(currentState));
      break;

    case EW_GREEN_STATE:
      // EW green, NS red
      setLights(LED_ON,  LED_OFF, LED_OFF,
                LED_OFF, LED_OFF, LED_ON);
      Serial.print("STATE: ");
      Serial.println(stateName(currentState));
      break;

    case EW_YELLOW_STATE:
      // EW yellow, NS red
      setLights(LED_ON,  LED_OFF, LED_OFF,
                LED_OFF, LED_ON,  LED_OFF);
      Serial.print("STATE: ");
      Serial.println(stateName(currentState));
      break;

    case PED_BOTH_YELLOW_STATE:
      // both yellow
      setLights(LED_OFF, LED_ON,  LED_OFF,
                LED_OFF, LED_ON,  LED_OFF);
      Serial.print("STATE: ");
      Serial.println(stateName(currentState));
      break;

    case PED_BOTH_RED_STATE:
      // both red
      setLights(LED_ON,  LED_OFF, LED_OFF,
                LED_ON,  LED_OFF, LED_OFF);
      Serial.print("STATE: ");
      Serial.println(stateName(currentState));
      break;
  }
}

// ================= BUTTON FUNCTIONS =================
bool checkButtonPressed(int pin, bool &lastReading, bool &stableReading, unsigned long &lastDebounce) {
  bool reading = digitalRead(pin);

  if (reading != lastReading) {
    lastDebounce = millis();
  }

  if ((millis() - lastDebounce) > DEBOUNCE_MS) {
    if (reading != stableReading) {
      stableReading = reading;

      // active LOW button
      if (stableReading == LOW) {
        lastReading = reading;
        return true;
      }
    }
  }

  lastReading = reading;
  return false;
}

void handleButtons() {
  bool btn1Pressed = checkButtonPressed(PED_BTN_1, lastReading1, stableReading1, lastDebounce1);
  bool btn2Pressed = checkButtonPressed(PED_BTN_2, lastReading2, stableReading2, lastDebounce2);

  if ((btn1Pressed || btn2Pressed) &&
      !pedWaiting &&
      currentState != PED_BOTH_YELLOW_STATE &&
      currentState != PED_BOTH_RED_STATE) {
    pedWaiting = true;
    pedRequestTime = millis();
    resumeState = currentState;
    lastPedCountdownSecond = -1;
    Serial.println("PEDESTRIAN REQUEST REGISTERED");
    Serial.print("PEDESTRIAN: will resume ");
    Serial.println(stateName(resumeState));
  }
}

// ================= SETUP =================
void setup() {
  Serial.begin(115200);

  pinMode(NS_RED, OUTPUT);
  pinMode(NS_YELLOW, OUTPUT);
  pinMode(NS_GREEN, OUTPUT);

  pinMode(EW_RED, OUTPUT);
  pinMode(EW_YELLOW, OUTPUT);
  pinMode(EW_GREEN, OUTPUT);

  pinMode(PED_BTN_1, INPUT_PULLUP);
  pinMode(PED_BTN_2, INPUT_PULLUP);

  allLightsOff();
  changeState(NS_GREEN_STATE);
}

// ================= LOOP =================
void loop() {
  handleButtons();

  unsigned long now = millis();
  unsigned long elapsed = now - stateStartTime;

  logPedestrianCountdown(now);
  logStateCountdown(now);

  // If pedestrian request has waited 3 sec, start pedestrian sequence.
  if (pedWaiting &&
      (currentState == NS_GREEN_STATE || currentState == NS_YELLOW_STATE ||
       currentState == EW_GREEN_STATE || currentState == EW_YELLOW_STATE) &&
      (now - pedRequestTime >= PED_WAIT)) {

    pedWaiting = false;
    lastPedCountdownSecond = -1;

    changeState(PED_BOTH_YELLOW_STATE);
    return;
  }

  switch (currentState) {
    case NS_GREEN_STATE:
      if (elapsed >= GREEN_TIME) {
        changeState(NS_YELLOW_STATE);
      }
      break;

    case NS_YELLOW_STATE:
      if (elapsed >= YELLOW_TIME) {
        changeState(EW_GREEN_STATE);
      }
      break;

    case EW_GREEN_STATE:
      if (elapsed >= GREEN_TIME) {
        changeState(EW_YELLOW_STATE);
      }
      break;

    case EW_YELLOW_STATE:
      if (elapsed >= YELLOW_TIME) {
        changeState(NS_GREEN_STATE);
      }
      break;

    case PED_BOTH_YELLOW_STATE:
      if (elapsed >= YELLOW_TIME) {
        changeState(PED_BOTH_RED_STATE);
      }
      break;

    case PED_BOTH_RED_STATE:
      if (elapsed >= PED_RED_TIME) {
        changeState(resumeState);
      }
      break;
  }
}

README

Download README.md

# Crossroad Traffic Light Controller

Arduino/ESP32 project for controlling a simple two-way crossroad traffic light system with pedestrian buttons.

The sketch is in `crossroad.ino`.

## Features

- Controls North/South and East/West traffic lights.
- Uses two pedestrian buttons.
- Runs with a non-blocking `millis()` state machine.
- Prints state changes and countdown logs to the Serial Monitor.
- Uses button debounce to avoid repeated false presses.

## Hardware

This project is written for an ESP32.

### Traffic Light Pins

| Direction | Color  | GPIO |
| --- | --- | --- |
| North/South | Red    | 23 |
| North/South | Yellow | 22 |
| North/South | Green  | 21 |
| East/West | Red    | 19 |
| East/West | Yellow | 18 |
| East/West | Green  | 5 |

### Pedestrian Button Pins

| Button | GPIO |
| --- | --- |
| Pedestrian button 1 | 32 |
| Pedestrian button 2 | 33 |

## Wiring Notes

### LEDs

The code uses active-low LEDs:

```cpp
const int LED_ON  = LOW;
const int LED_OFF = HIGH;
```

That means an LED turns on when the ESP32 pin is `LOW`.

Typical wiring:

```text
3.3V -> resistor -> LED -> GPIO pin
```

If your LEDs are wired from GPIO to resistor to GND, the lights may behave inverted.

### Buttons

The pedestrian buttons use `INPUT_PULLUP`:

```cpp
pinMode(PED_BTN_1, INPUT_PULLUP);
pinMode(PED_BTN_2, INPUT_PULLUP);
```

Each button should be wired like this:

```text
GPIO 32 -> button -> GND
GPIO 33 -> button -> GND
```

When the button is not pressed, the pin reads `HIGH`.
When the button is pressed, the pin connects to GND and reads `LOW`.

## Timing

| Event | Duration |
| --- | --- |
| Normal green light | 15 seconds |
| Normal yellow light | 3 seconds |
| Wait after pedestrian button press | 3 seconds |
| Pedestrian both-yellow phase | 3 seconds |
| Pedestrian both-red phase | 10 seconds |
| Button debounce | 50 milliseconds |

## Normal Traffic Flow

The normal traffic cycle is:

```text
NS green, EW red
NS yellow, EW red
EW green, NS red
EW yellow, NS red
repeat
```

## Pedestrian Flow

When either pedestrian button is pressed:

1. The request is registered.
2. The controller waits 3 seconds.
3. Both directions turn yellow.
4. After 3 seconds, both directions turn red.
5. After 10 seconds, the controller resumes the traffic state that was active when the button was pressed.

## Serial Monitor

Open the Serial Monitor at:

```text
115200 baud
```

Example logs:

```text
STATE: NS GREEN, EW RED
TIME BEFORE CHANGE (NS GREEN, EW RED): 15 sec
TIME BEFORE CHANGE (NS GREEN, EW RED): 14 sec
PEDESTRIAN REQUEST REGISTERED
PEDESTRIAN: will resume NS GREEN, EW RED
PEDESTRIAN: both yellow starts in 3 sec
STATE: BOTH YELLOW
TIME BEFORE CHANGE (BOTH YELLOW): 3 sec
STATE: BOTH RED
TIME BEFORE CHANGE (BOTH RED): 10 sec
STATE: NS GREEN, EW RED
```

## Uploading

1. Open `crossroad.ino` in the Arduino IDE.
2. Select your ESP32 board.
3. Select the correct USB port.
4. Upload the sketch.
5. Open Serial Monitor at `115200` baud to see logs.

~