Zach Christensen
arduino journey
P04bTiming and BrightnessSession 4✓ Working

Traffic Light + WALK

A fourth state, a pedestrian LED with its own rhythm, and typing commands into the Serial Monitor.

Carried forward fromP04 Traffic Light
Running
Serial Monitor
Notebook

Worked out by hand before anything got wired. Why that matters.

The sketch

04b_Traffic_Light_Walk.ino
const uint8_t PIN_RED = 9;const uint8_t PIN_YEL = 10;const uint8_t PIN_GRN = 11;const uint8_t PIN_WLK = 5;const uint8_t PIN_BTN = 2;const unsigned long WALK_MS = 4000;const uint8_t WALK_FLASHES = 4;unsigned int pressCount = 0;enum class Light : uint8_t { RED, GREEN, YELLOW, WALK }; // create the Light typeLight state = Light::RED;unsigned long stateStart = 0;bool walkRequested = false;bool lastPressed = false;void setLight(bool r , bool y, bool g, bool w) { // bool -> digitalWrite conversion works because HIGH and LOW is just #defines for 1 and 0  digitalWrite(PIN_RED, r); // sets the voltage on a pin that is already configured as OUTPUT  digitalWrite(PIN_YEL, y);  digitalWrite(PIN_GRN, g);  digitalWrite(PIN_WLK, w); }void setup() {  pinMode(PIN_RED, OUTPUT); // configures pins electrical direction  pinMode(PIN_YEL, OUTPUT);  pinMode(PIN_GRN, OUTPUT);  pinMode(PIN_WLK, OUTPUT);  pinMode(PIN_BTN, INPUT_PULLUP);  Serial.begin(9600); // sets up UART hardware at 9600 bits per second  stateStart = millis();  Serial.println("Starting state: RED");}void loop() {  unsigned long elapsed = millis() - stateStart; //computed fresh on every pass    // poll the button on EVERY iteration. This is the whole point  // With delay() in the loop, a press during the wait would be missed  bool pressed = (digitalRead(PIN_BTN) == LOW); // is the button pressed right now?    if (pressed && !lastPressed) { // the initial press case (falling edge)    walkRequested = true; // sets a flag that survuves across thousands of loop iterations until the state machine consumes it    pressCount ++;    Serial.println("[walk requested]");    Serial.println(pressCount);  }    lastPressed = pressed;  if (Serial.available() > 0) { // checking for interrupt    char key = Serial.read();    switch (key) {    case 'r':      state = Light::RED;      stateStart = millis();      Serial.println("! forced RED");      break;    case 'g':      state = Light::GREEN;      stateStart = millis();      Serial.println("! forced GREEN");      break;    case 'y':      state = Light::YELLOW;      stateStart = millis();      Serial.println("! forced YELLOW");      break;    case 'w':      state = Light::WALK;      stateStart = millis();      Serial.println("! forced WALK");      break;    }  }  /* the traffic-light state machine    every case has the same 2 parts:       1. do the states job (drive LED to match current state)      2. check whether it is time to leave - and if so, change state and re-stamp stateStart  */  switch (state) {    case Light::RED:      setLight(true, false, false, false);      if (elapsed >= 4000) {        state = Light::GREEN;         stateStart = millis();        Serial.println("-> GREEN");      }      break;        // green normally lasts 6s. But if a pedestrian is waiting (walk requested == true), cut it short    // once cars have had a fair 1.5s minimum.    case Light::GREEN:      setLight(false, false, true, false);      if (elapsed >= 6000 || (walkRequested && elapsed >= 1500)) {        state = Light::YELLOW;         stateStart = millis();        Serial.println("-> YELLOW");      }      break;    case Light::YELLOW:      setLight(false, true, false, false);      if (elapsed >= 1500) {        if (walkRequested) {          state = Light::WALK;          Serial.println("-> WALK"); // print inside each branch, or the log lies about where we went        }        else {          state = Light::RED;          Serial.println("-> RED");        }        stateStart = millis();      }      break;    case Light::WALK: {      const unsigned long HALF = WALK_MS / (2 * WALK_FLASHES); // 500      bool blinkOn = ((elapsed / HALF) % 2 == 0);      setLight(true, false, false, blinkOn);      if (elapsed >= WALK_MS) {        state = Light::GREEN;         stateStart = millis();        walkRequested = false; // request satisfied        Serial.println("-> GREEN");      }    }      break;  }}

Traffic Light with all four of the book's "try this next" exercises done. A real WALK state instead of just cutting green short, a pedestrian LED blinking on its own 500ms rhythm while the main machine keeps running, r g y w typed into the Serial Monitor to force a state, and a press counter to make bounce countable.

Cycle is RED 4s, GREEN 6s (or 1.5s if someone is waiting), YELLOW 1.5s, WALK 4s, then back to GREEN. During WALK the car lights hold red and the walk LED on D5 blinks four times.

Adding a state moved the flag

P04 taught that you clear a flag where the request is satisfied. What it could not show is that "where" is not fixed.

Adding WALK between YELLOW and GREEN meant walkRequested had to move its clear from the GREEN to YELLOW transition down to WALK to GREEN, because WALK is now the thing that actually satisfies the request. A flag is a contract between whoever sets it and whoever clears it, and inserting a state silently rewrites that contract. Nothing in the compiler catches it. The old code still compiles and still runs, just wrong.

Also worth knowing: RED and WALK both light the red LED, so they look identical on the breadboard. They are different states because their transitions differ, not their outputs. If the difference is only a number, use data. If it is behaviour, use a state.

The log line that lied

YELLOW has two exits but started with one print, sitting after the if/else where both paths converge. So going into WALK announced itself as -> RED. The lights were doing the right thing and the log was lying about it, and because the LEDs go red during WALK anyway, the display backed up the wrong story.

Fix was moving each print inside its own branch, next to the assignment it describes. stateStart = millis() does belong after the merge, because it is genuinely common to both paths. Telling those two apart is the whole lesson.

What I learned