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.