Developmental Project 8.0 IR and Frog Tag

By experimenting with variables and functions, students will deepen their understanding of how timing, conditionals, and outputs work in an interactive system.

By experimenting with variables and functions, students will deepen their understanding of how timing, conditionals, and outputs work in an interactive system.

Sample Code:

				
					#include "1ST_Maker_Frog.h"

const uint8_t  TAG_ADDRESS = 0x00;  // NEC uses 8-bit address
const uint8_t  TAG_COMMAND = 0x55;  // tag message
const uint8_t  HIT_COMMAND = 0x77;  // "I'm hit" message

bool isIt = false;      // true = IT, false = SAFE
bool wasIt = false;     // debounce for sending TAG
bool lastIsIt = false;  // for NeoPixel steady-state updates

// Cooldown handling for the SAFE player after being tagged
bool cooldownActive = false;
unsigned long cooldownStart = 0;
const unsigned long COOLDOWN_MS = 5000;

// Repeated "I'm hit" resends during cooldown (to ensure IT hears it)
unsigned long lastHitResend = 0;
const unsigned long HIT_RESEND_INTERVAL_MS = 200; // ~5 msgs/sec

// Cooldown visual (alternate eyes red/green every 500 ms)
bool cooldownBlinkState = false;
unsigned long lastBlinkToggle = 0;
const unsigned long BLINK_INTERVAL_MS = 500;

/**************************************************
 * Function: setup
 * Purpose:  Initializes peripherals, display, IR,
 *           and performs initial role assignment.
 **************************************************/
void setup() {
  Serial.begin(9600);
  delay(100);

  IrReceiver.begin(IRRxPin);
  IrSender.begin(IRTxPin);

  pinMode(BUTTON_ONE, INPUT_PULLUP);
  pinMode(BUTTON_TWO, INPUT_PULLUP);

  pixel.begin();
  pixel.clear();
  pixel.show();

  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
  display.clearDisplay();
  display.display();

  initialAssignment();   // sets isIt and updates eyes
}

/**************************************************
 * Function: loop
 * Purpose:  Main program loop; processes IR, handles
 *           cooldown, checks inputs, updates display
 *           and NeoPixels.
 **************************************************/
void loop() {
  handleIR();
  handleCooldown();
  handleInputAndTag();
  updateDisplay();
  updateEyes();          // steady or cooldown animation
}

/**************************************************
 * Function: handleInputAndTag
 * Purpose:  Handles button input for the IT player
 *           and sends TAG messages.
 **************************************************/
void handleInputAndTag() {
  // Only IT is allowed to send TAG via button press
  if (isIt) {
    if (digitalRead(BUTTON_ONE) == PRESSED || digitalRead(BUTTON_TWO) == PRESSED) {
      if (!wasIt) {
        Serial.println("TAG -> sent");
        IrSender.sendNEC(TAG_ADDRESS, TAG_COMMAND, 0);
        tone(PIEZO, 1000, 100);
        delay(250);  // simple debounce so holding the button doesn't spam
      }
      wasIt = true;
    } else {
      wasIt = false;
    }
  }
}

/**************************************************
 * Function: handleIR
 * Purpose:  Processes incoming IR signals and 
 *           determines whether to switch roles.
 **************************************************/
void handleIR() {
  if (!IrReceiver.decode()) return;

  if (IrReceiver.decodedIRData.protocol == NEC) {
    uint16_t receivedAddress = IrReceiver.decodedIRData.address;
    uint8_t  receivedCommand = IrReceiver.decodedIRData.command;

    Serial.print("IR RX - NEC, Addr 0x");
    Serial.print(receivedAddress, HEX);
    Serial.print(", Cmd 0x");
    Serial.println(receivedCommand, HEX);

    if (receivedAddress == TAG_ADDRESS) {
      if (receivedCommand == TAG_COMMAND) {
        // SAFE got tagged -> start cooldown and broadcast "I'm hit"
        if (!isIt && !cooldownActive) {
          Serial.println("Received TAG while SAFE -> starting cooldown, sending IM_HIT");
          startCooldown();
          sendImHit(); // immediate first send
          tone(PIEZO, 700, 120);
        }
      } else if (receivedCommand == HIT_COMMAND) {
        // IT heard the "I'm hit" -> immediately become SAFE
        if (isIt) {
          Serial.println("Received IM_HIT while IT -> switching to SAFE");
          isIt = false;
          tone(PIEZO, 400, 120);
          wasIt = false; // reset debounce
        }
      }
    }
  } else {
    Serial.print("Non-NEC protocol: ");
    Serial.println((int)IrReceiver.decodedIRData.protocol);
  }
  IrReceiver.resume();
}

/**************************************************
 * Function: startCooldown
 * Purpose:  Initiates cooldown sequence for SAFE 
 *           after being tagged.
 **************************************************/
void startCooldown() {
  cooldownActive = true;
  cooldownStart = millis();
  lastHitResend = 0;     // force an immediate resend on next tick
  cooldownBlinkState = false;
  lastBlinkToggle = 0;   // force immediate first blink frame
}

/**************************************************
 * Function: handleCooldown
 * Purpose:  Manages cooldown timing, re-sends IM_HIT
 *           and switches SAFE to IT after time expires.
 **************************************************/
void handleCooldown() {
  if (!cooldownActive) return;

  unsigned long now = millis();

  // Re-send IM_HIT periodically during the cooldown window
  if (lastHitResend == 0 || (now - lastHitResend) >= HIT_RESEND_INTERVAL_MS) {
    sendImHit();
    lastHitResend = now;
  }

  // When cooldown expires, SAFE becomes IT
  if (now - cooldownStart >= COOLDOWN_MS) {
    cooldownActive = false;
    Serial.println("Cooldown over -> SAFE becomes IT");
    isIt = true;
    // Ensure next updateEyes() pushes steady IT color immediately
    lastIsIt = !isIt;          // force a refresh of steady color
    cooldownBlinkState = false;
    lastBlinkToggle = 0;
    tone(PIEZO, 550, 150);
  }
}

/**************************************************
 * Function: sendImHit
 * Purpose:  Sends an "I'm hit" message via IR.
 **************************************************/
void sendImHit() {
  IrSender.sendNEC(TAG_ADDRESS, HIT_COMMAND, 0);
  Serial.println("IM_HIT -> sent");
}

/**************************************************
 * Function: initialAssignment
 * Purpose:  Prompts user to choose initial role (IT
 *           or SAFE) with buttons.
 **************************************************/
void initialAssignment() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("SW 1: IT");
  display.println("SW 2: SAFE");
  display.display();

  while (true) {
    if (digitalRead(BUTTON_ONE) == PRESSED) {
      // Wait for release
      while (digitalRead(BUTTON_ONE) == PRESSED) {
        delay(10);
      }
      delay(50);  // debounce
      isIt = true;
      break;
    }
    if (digitalRead(BUTTON_TWO) == PRESSED) {
      // Wait for release
      while (digitalRead(BUTTON_TWO) == PRESSED) {
        delay(10);
      }
      delay(50);  // debounce
      isIt = false;
      break;
    }
    delay(10);
  }
  lastIsIt = !isIt;  // force updateEyes to light LEDs immediately
  updateEyes();   // set NeoPixels after initial choice
}

/**************************************************
 * Function: updateDisplay
 * Purpose:  Updates the OLED display with role or
 *           cooldown status.
 **************************************************/
void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);

  // During cooldown, show "I'm hit!" prominently
  if (cooldownActive) {
    display.println(" I'm hit!");
  } else if (isIt) {
    display.println("  I'm IT!");
  } else {
    display.println(" I'm safe!");
  }

  // Small status line
  display.setTextSize(1);
  display.setCursor(30, 48);
  if (cooldownActive) {
    unsigned long remaining = (COOLDOWN_MS - (millis() - cooldownStart)) / 1000;
    display.print("Cooldown: ");
    display.print((int)remaining);
    display.println("s");
  } else {
    display.println("    Ready");
  }

  display.display();
}

/**************************************************
 * Function: updateEyes
 * Purpose:  Updates NeoPixel eyes: steady colors
 *           for roles or alternating in cooldown.
 **************************************************/
void updateEyes() {
  // During cooldown: alternate eyes red/green every 500 ms
  if (cooldownActive) {
    unsigned long now = millis();
    if (lastBlinkToggle == 0 || (now - lastBlinkToggle) >= BLINK_INTERVAL_MS) {
      lastBlinkToggle = now;
      cooldownBlinkState = !cooldownBlinkState;

      uint32_t green = pixel.Color(0, 255, 0);
      uint32_t red   = pixel.Color(255, 0, 0);

      if (cooldownBlinkState) {     // during cooldown, alternate the eye colors
        pixel.setPixelColor(0, green); 
        pixel.setPixelColor(1, red);
      } else {
        pixel.setPixelColor(0, red);   
        pixel.setPixelColor(1, green);
      }
      pixel.show();
    }
    return; // don't do steady-state updates while animating
  }

  // Steady-state updates only when role actually changes
  if (isIt != lastIsIt) {
    uint32_t c;
    if (isIt) {
      c = pixel.Color(255, 0, 0);   // red = IT
    } else {
      c = pixel.Color(0, 255, 0);   // green = SAFE
    }
    pixel.setPixelColor(0, c);
    pixel.setPixelColor(1, c);
    pixel.show();
    lastIsIt = isIt;
  }
}
				
			

*If you’re copying and pasting the code, or typing from scratch, delete everything out of a new Arduino sketch and paste / type in the above text.

Learn More In Our Free Instructional Guidebook

This comprehensive guidebook for the 1st Maker Space Microcontroller Trainer provides a comprehensive introduction to the world of Arduino programming for beginners. It guides users through the foundational concepts of microcontrollers, detailing the unique features of the Arduino Leonardo-compatible MCU Trainer board. The manual offers a step-by-step journey from understanding the hardware components and the Arduino programming language to the vibrant global community of Arduino enthusiasts. It delves into the intricacies of each onboard circuit, explaining their functionalities and applications. With a focus on hands-on learning, the manual includes a series of coding exercises, tutorials in C/C++, and insights into the Arduino IDE.

More Projects

Developmental Project 1.00 Vote

In this project, vote using the OLED and buttons.

Developmental Project 2.00 Mind the Dog

The program demonstrates how embedded systems can track time without stopping the program. Instead of using delay(), which pauses execution, it uses the millis() timestamp technique to calculate elapsed time. This allows multiple tasks—timers, display updates, and LED indicators—to run simultaneously.