// ============== WHAT LIBRARIES WE'RE USING ==============
// Think of libraries like toolboxes - they contain pre-made tools we can use
#include "DFRobotDFPlayerMini.h" // Toolbox for controlling a sound player
#include <SoftwareSerial.h> // Toolbox for talking to the sound player
#include <Adafruit_NeoPixel.h> // Toolbox for controlling colorful LED lights
// ============== NAMING THE WIRES (PIN DEFINITIONS) ==============
// Instead of remembering "wire goes to hole 2", we give each wire a name
// Communication wires for the sound player
#define SERIAL_RX_PIN 2 // Wire that receives messages from sound player
#define SERIAL_TX_PIN 3 // Wire that sends messages to sound player
// Wires for the colorful LED strips
#define POWERCELL_LED_PIN 4 // Wire controlling the main LED strip
#define NOSE_LED_PIN 5 // Wire controlling the nose LED strip
// Wires for regular on/off lights
#define LED_PIN_6 6 // Wire for light #6
#define LED_PIN_7 7 // Wire for light #7
#define LED_PIN_8 8 // Wire for light #8
// Wires for buttons and switches the user can press
#define TOGGLE_1_PIN 9 // Wire for switch #1 (starts vent mode)
#define TOGGLE_2_PIN 10 // Wire for switch #2 (turns system on/off)
#define PUSH_1_PIN 11 // Wire for button #1 (slime mode when held)
#define PUSH_2_PIN 12 // Wire for button #2 (changes songs)
// Wire for controlling other devices
#define RELAY_PIN 13 // Wire that can turn other things on/off
// ============== SETTINGS FOR THE LED STRIPS ==============
#define POWERCELL_PIXELS 7 // How many lights are in the main strip
#define NOSE_PIXELS 8 // How many lights are in the nose strip
#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // What type of LED strips we have
// ============== NAMING THE SOUND FILES ==============
// The sound player reads files numbered 0001.mp3, 0002.wav, etc.
// We give each file a name so we remember what it does
#define AUDIO_IDLE 1 // File #1 - quiet background sound
#define AUDIO_VENT 2 // File #2 - whooshing vent sound
#define AUDIO_BOOT 3 // File #3 - startup sound
#define AUDIO_SHUTDOWN 4 // File #4 - shutdown sound
#define AUDIO_SLIME 5 // File #5 - slime sound (repeats while button held)
#define AUDIO_SLIME_END 6 // File #6 - slime ending sound
#define AUDIO_SONG1 7 // File #7 - music song #1
#define AUDIO_SONG2 8 // File #8 - music song #2
#define AUDIO_SONG3 9 // File #9 - music song #3
#define AUDIO_SONG4 10 // File #10 - music song #4
// ============== TIMING SETTINGS ==============
// These numbers control how long things take (measured in milliseconds)
// 1000 milliseconds = 1 second
#define BUTTON_DEBOUNCE_TIME 50 // Wait 0.05 seconds to make sure button press is real
#define VENT_FLASH_DELAY 200 // Wait 0.2 seconds between light flashes in vent mode
#define VENT_FLASH_COUNT 6 // Flash the lights 6 times during vent mode
#define VENT_INITIAL_DELAY 1000 // Wait 1 second before starting vent light show
#define VENT_END_DELAY 3000 // Wait 3 seconds at end of vent mode before cleaning up
#define BOOT_FLASH_DELAY 200 // Wait 0.2 seconds between light flashes when starting up
#define BOOT_FLASH_COUNT 5 // Flash the lights 5 times when starting up
#define SHUTDOWN_DELAY 4000 // Wait 4 seconds before starting shutdown light show
#define SHUTDOWN_LED_DELAY 500 // Wait 0.5 seconds between turning off each light
#define SLIME_END_DELAY 4000 // Slime ending lasts 4 seconds
#define IDLE_FLASH_DELAY 20 // Very fast flashing (0.02 seconds) for special effects
#define IDLE_SEQUENCE_DELAY 200 // Wait 0.2 seconds between steps in idle light show
#define AUDIO_INIT_TIMEOUT 5000 // Give up trying to start sound after 5 seconds
#define AUDIO_RETRY_INTERVAL 10000 // Try to start sound again every 10 seconds
// ============== SOUND SETTINGS ==============
#define DEFAULT_VOLUME 20 // Normal volume (scale of 0-30, where 30 is loudest)
#define LOUD_VOLUME 28 // Loud volume for dramatic effects
#define BAUD_RATE 9600 // Speed for talking to the sound player
// ============== COLOR RECIPES ==============
// Each color is made by mixing Red, Green, and Blue light (0-255 each)
// Think of it like mixing paint - different amounts make different colors
#define COLOR_OFF 0, 0, 0 // No light = off
#define COLOR_RED 255, 0, 0 // Full red, no green, no blue = bright red
#define COLOR_GREEN 0, 150, 0 // No red, medium green, no blue = green
#define COLOR_BLUE 0, 0, 255 // No red, no green, full blue = bright blue
#define COLOR_MAGENTA 255, 0, 255 // Full red + full blue = purple/magenta
#define COLOR_WHITE 255, 255, 255 // All colors full = white
#define COLOR_ORANGE 255, 150, 0 // Full red + medium green = orange
#define COLOR_DIM_RED 5, 0, 0 // Tiny bit of red = very dim red
#define COLOR_DIM_RED_2 100, 0, 0 // Some red = medium dim red
#define COLOR_PINK 255, 10, 70 // Mostly red + tiny green + some blue = pink
#define COLOR_YELLOW 255, 255, 0 // Full red + full green = yellow
// ============== SYSTEM MODES ==============
// These are like different "modes" the system can be in - only one at a time
enum SystemState {
STATE_IDLE, // Just sitting there, waiting for user input
STATE_VENT_MODE, // Playing vent sequence with lights and sound
STATE_BOOT_MODE, // Playing startup sequence
STATE_SHUTDOWN_MODE, // Playing shutdown sequence
STATE_SLIME_MODE, // Playing slime effect while button is held
STATE_SLIME_ENDING, // Playing slime ending after button released
STATE_SONG_PLAYING // Playing music
};
// ============== ANIMATION TYPES ==============
// These control exactly what the lights are doing within each mode
enum AnimationState {
ANIM_IDLE, // Lights not doing anything special
ANIM_VENT_INITIAL, // First part of vent mode (waiting)
ANIM_VENT_FLASHING, // Second part of vent mode (lights flashing)
ANIM_VENT_ENDING, // Last part of vent mode (cleaning up)
ANIM_BOOT_FLASHING, // Lights flashing during startup
ANIM_SHUTDOWN_PROGRESSION, // Lights turning off one by one during shutdown
ANIM_SLIME_FLASHING, // Lights flashing during slime mode
ANIM_SLIME_ENDING, // Lights during slime ending
ANIM_IDLE_SEQUENCE // Special light sequence when idle
};
// ============== SOUND SYSTEM STATUS ==============
// These track whether the sound system is working or not
enum AudioState {
AUDIO_NOT_INITIALIZED, // Haven't tried to start sound system yet
AUDIO_INITIALIZING, // Currently trying to start sound system
AUDIO_READY, // Sound system is working and ready
AUDIO_FAILED // Sound system failed to start
};
// ============== CREATING OUR TOOLS ==============
// These create the actual tools we'll use to control things
SoftwareSerial dfPlayerSerial(SERIAL_RX_PIN, SERIAL_TX_PIN); // Tool for talking to sound player
DFRobotDFPlayerMini dfPlayer; // Tool for controlling sound player
Adafruit_NeoPixel powercell = Adafruit_NeoPixel(POWERCELL_PIXELS, POWERCELL_LED_PIN, NEOPIXEL_TYPE); // Tool for main LED strip
Adafruit_NeoPixel nose = Adafruit_NeoPixel(NOSE_PIXELS, NOSE_LED_PIN, NEOPIXEL_TYPE); // Tool for nose LED strip
// ============== MEMORY BOXES FOR TRACKING THINGS ==============
// These are like boxes where we store information the system needs to remember
// What mode is the system in right now?
SystemState currentState = STATE_IDLE; // Start in idle mode
AnimationState animState = ANIM_IDLE; // Start with no animation
AudioState audioState = AUDIO_NOT_INITIALIZED; // Sound system not started yet
// Music-related memory
int currentSong = 0; // Which song is selected (0 = none)
bool canChangeSong = true; // Is it okay to change songs right now?
int idleCounts = 500; // Counter used for idle effects
// ============== MEMORY FOR BUTTON STATES ==============
// We need to remember what buttons were doing before, so we can tell when they change
bool toggle1State = true; // Is toggle 1 ready to be used?
bool toggle2State = true; // Is toggle 2 ready to be used?
bool shutdownToggle2 = false; // Is toggle 2 ready to trigger shutdown?
bool lastPush1 = false; // Was push button 1 pressed last time we checked?
bool lastPush2 = false; // Was push button 2 pressed last time we checked?
bool lastTog1 = false; // Was toggle 1 on last time we checked?
bool lastTog2 = false; // Was toggle 2 on last time we checked?
// ============== MEMORY FOR TIMING ==============
// These remember when things happened, so we can time events properly
unsigned long lastDebounceTime = 0; // When did we last check the buttons?
unsigned long animationTimer = 0; // When did the current animation step start?
unsigned long modeTimer = 0; // When did the current mode start?
unsigned long flashTimer = 0; // When did the lights last flash?
unsigned long audioInitTimer = 0; // When did we start trying to initialize sound?
unsigned long audioRetryTimer = 0; // When should we try sound initialization again?
// ============== MEMORY FOR ANIMATIONS ==============
// These help control multi-step light shows
int flashCounter = 0; // How many times have the lights flashed?
int animationStep = 0; // What step of the animation are we on?
bool flashState = false; // Are the flashing lights currently on or off?
int audioInitAttempts = 0; // How many times have we tried to start sound?
// ============== SETUP FUNCTION - RUNS ONCE WHEN SYSTEM STARTS ==============
void setup() {
// Start communication with computer for debugging messages
Serial.begin(115200);
Serial.println("Interactive Prop Controller Starting...");
// Set up all our hardware in the right order
initializeNeoPixels(); // Get the LED strips ready
configurePins(); // Tell the system which wires do what
setInitialState(); // Set starting conditions and show we're starting up
startAudioInitialization(); // Try to get the sound system working (but don't wait for it)
Serial.println("System ready - functioning with or without audio");
}
// ============== MAIN LOOP - RUNS OVER AND OVER FOREVER ==============
void loop() {
// Get the current time once (instead of asking for it multiple times)
unsigned long currentTime = millis(); // This gives us milliseconds since the system started
// Do all the main jobs every time through the loop
updateAudioSystem(currentTime); // Check on the sound system
handleInputs(currentTime); // See what buttons/switches the user is doing
updateAnimations(currentTime); // Update any light shows that are running
updateSystemState(currentTime); // Update what mode the system is in
updateStatusIndicator(); // Update the status light that shows if sound is working
}
// ============== SOUND SYSTEM STARTUP FUNCTIONS ==============
/**
* This function tries to start up the sound system
* It doesn't wait around - it just starts the process
*/
void startAudioInitialization() {
dfPlayerSerial.begin(BAUD_RATE); // Start talking to the sound player
audioInitTimer = millis(); // Remember when we started trying
audioState = AUDIO_INITIALIZING; // Mark that we're trying to initialize
audioInitAttempts++; // Count this as an attempt
Serial.println("Attempting DFPlayer Pro initialization (attempt " + String(audioInitAttempts) + ")...");
}
/**
* This function checks on the sound system and handles retries
* It gets called every time through the main loop
*/
void updateAudioSystem(unsigned long currentTime) {
// Check what state the sound system is in and act accordingly
switch (audioState) {
case AUDIO_NOT_INITIALIZED:
// We're not trying to initialize right now - should we start trying again?
if (currentTime - audioRetryTimer >= AUDIO_RETRY_INTERVAL) {
startAudioInitialization(); // Yes, enough time has passed - try again
}
break;
case AUDIO_INITIALIZING:
// We're currently trying to initialize - has it been long enough to check if it worked?
if (currentTime - audioInitTimer >= 1000) { // Wait 1 second
if (dfPlayer.begin(dfPlayerSerial)) {
// Success! The sound system is working
Serial.println("DFPlayer Pro initialized successfully!");
// Set up the sound system with our preferred settings
dfPlayer.volume(DEFAULT_VOLUME); // Set volume to normal level
dfPlayer.EQ(DFPLAYER_EQ_NORMAL); // Set sound quality to normal
dfPlayer.outputDevice(DFPLAYER_DEVICE_SD); // Use the SD card for music files
audioState = AUDIO_READY; // Mark sound system as ready
Serial.println("Audio system ready");
} else {
// Failed to initialize - should we try again or give up?
if (audioInitAttempts >= 3) {
// We've tried 3 times - give up for now
Serial.println("DFPlayer Pro initialization failed after 3 attempts - continuing without audio");
audioState = AUDIO_FAILED;
} else {
// Try again later
Serial.println("DFPlayer Pro initialization failed, will retry in 10 seconds");
audioState = AUDIO_NOT_INITIALIZED;
audioRetryTimer = currentTime; // Remember when to try again
}
}
}
break;
case AUDIO_FAILED:
// Sound system failed, but let's try again occasionally (every 30 seconds)
if (currentTime - audioRetryTimer >= AUDIO_RETRY_INTERVAL * 3) {
Serial.println("Retrying audio initialization...");
audioState = AUDIO_NOT_INITIALIZED;
audioInitAttempts = 0; // Reset counter - give it fresh tries
audioRetryTimer = currentTime;
}
break;
case AUDIO_READY:
// Sound system is working fine - nothing to do
break;
}
}
/**
* This function sets up the LED strips so we can control them
*/
void initializeNeoPixels() {
// Special fix needed for certain microcontrollers
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
clock_prescale_set(clock_div_1);
#endif
powercell.begin(); // Get the main LED strip ready
nose.begin(); // Get the nose LED strip ready
Serial.println("NeoPixels initialized");
}
/**
* This function tells the system what each wire is for
*/
void configurePins() {
// Set up input wires (buttons and switches) with pull-up resistors
// Pull-up resistors make unpressed buttons read as HIGH, pressed as LOW
pinMode(TOGGLE_1_PIN, INPUT_PULLUP); // Toggle switch 1
pinMode(TOGGLE_2_PIN, INPUT_PULLUP); // Toggle switch 2
pinMode(PUSH_1_PIN, INPUT_PULLUP); // Push button 1
pinMode(PUSH_2_PIN, INPUT_PULLUP); // Push button 2
// Set up output wires (things we control)
pinMode(LED_PIN_6, OUTPUT); // Regular LED 6
pinMode(LED_PIN_7, OUTPUT); // Regular LED 7
pinMode(LED_PIN_8, OUTPUT); // Regular LED 8
pinMode(RELAY_PIN, OUTPUT); // Relay (can control other devices)
}
/**
* This function sets up how everything looks when the system first starts
*/
void setInitialState() {
// Turn on the first nose LED in yellow to show we're starting up
nose.setPixelColor(0, nose.Color(COLOR_YELLOW));
nose.show(); // Actually make the LED change color
// Turn off all the main LEDs and the relay
setMainLEDs(false); // Turn off regular LEDs
digitalWrite(RELAY_PIN, LOW); // Turn off relay
// Set the system to idle mode
currentState = STATE_IDLE;
animState = ANIM_IDLE;
}
/**
* This function updates a status light to show if the sound system is working
* It uses the first LED in the nose strip when the system is idle
*/
void updateStatusIndicator() {
// Only show status when idle - don't interfere with other light shows
if (currentState == STATE_IDLE && animState == ANIM_IDLE) {
// Show different colors based on sound system status
switch (audioState) {
case AUDIO_NOT_INITIALIZED:
case AUDIO_INITIALIZING:
// Blink yellow slowly while trying to start sound system
if ((millis() / 1000) % 2 == 0) { // Every other second
nose.setPixelColor(0, nose.Color(COLOR_YELLOW));
} else {
nose.setPixelColor(0, nose.Color(COLOR_OFF));
}
nose.show();
break;
case AUDIO_READY:
// Steady dim green when sound is working
nose.setPixelColor(0, nose.Color(0, 50, 0));
nose.show();
break;
case AUDIO_FAILED:
// Steady dim red when sound has failed
nose.setPixelColor(0, nose.Color(COLOR_DIM_RED));
nose.show();
break;
}
}
}
// ============== SOUND HELPER FUNCTIONS ==============
// These functions make it easier to control sound
/**
* Play a sound file (if sound system is working)
* @param fileNumber - Which sound file to play (1, 2, 3, etc.)
*/
void playAudioFile(int fileNumber) {
if (audioState == AUDIO_READY) {
dfPlayer.play(fileNumber); // Tell sound player to play the file
Serial.println("Playing audio file: " + String(fileNumber));
} else {
// Sound not working, but tell us what would have played
Serial.println("Audio not available - would play file: " + String(fileNumber));
}
}
/**
* Change the volume (if sound system is working)
* @param volume - How loud (0 = silent, 30 = loudest)
*/
void setVolume(int volume) {
if (audioState == AUDIO_READY) {
dfPlayer.volume(volume);
}
}
/**
* Pause the sound (if sound system is working)
*/
void pauseAudio() {
if (audioState == AUDIO_READY) {
dfPlayer.pause();
}
}
/**
* Resume playing sound (if sound system is working)
*/
void resumeAudio() {
if (audioState == AUDIO_READY) {
dfPlayer.start();
}
}
/**
* Check if sound is currently playing
* @return true if sound is playing, false if not
*/
bool isAudioPlaying() {
if (audioState == AUDIO_READY) {
return dfPlayer.readState() == 513; // 513 is the code for "playing"
}
return false; // If no sound system, nothing is playing
}
/**
* Check if sound system is ready to use
* @return true if sound system works, false if not
*/
bool isAudioAvailable() {
return (audioState == AUDIO_READY);
}
// ============== INPUT HANDLING FUNCTIONS ==============
/**
* This function checks what buttons and switches the user is doing
* It includes "debouncing" - waiting a bit to make sure button presses are real
*/
void handleInputs(unsigned long currentTime) {
// Read what all the buttons and switches are doing right now
// Remember: because of pull-up resistors, LOW = pressed/on, HIGH = not pressed/off
bool tog1 = (digitalRead(TOGGLE_1_PIN) == LOW); // Is toggle 1 switched on?
bool tog2 = (digitalRead(TOGGLE_2_PIN) == LOW); // Is toggle 2 switched on?
bool push1 = (digitalRead(PUSH_1_PIN) == LOW); // Is push button 1 pressed?
bool push2 = (digitalRead(PUSH_2_PIN) == LOW); // Is push button 2 pressed?
// Only check for button changes if enough time has passed (debouncing)
// This prevents false triggers from electrical noise
if (currentTime - lastDebounceTime > BUTTON_DEBOUNCE_TIME) {
// ---- Handle song selection (push button 2) ----
// Only works when system is idle and we haven't changed songs too recently
if (push2 && !lastPush2 && canChangeSong && currentState == STATE_IDLE) {
// Button just got pressed - cycle to next song
currentSong++; // Go to next song number
if (currentSong > 4) currentSong = 1; // After song 4, go back to song 1
startSongPlayback(currentSong); // Start playing the selected song
canChangeSong = false; // Don't allow rapid song changes
} else if (!push2) {
// Button is not pressed - allow song changes again
canChangeSong = true;
}
// ---- Handle toggle 1 (vent mode) ----
// Only works when system is idle and toggle is ready
if (tog1 && !lastTog1 && toggle1State && currentState == STATE_IDLE) {
// Toggle just got switched on - start vent mode
startVentMode(); // Begin vent sequence
toggle1State = false; // Don't trigger again until toggle is switched off
toggle2State = true; // Reset toggle 2 to ready state
} else if (!tog1) {
// Toggle is off - make it ready to trigger again
toggle1State = true;
}
// ---- Handle toggle 2 (boot/shutdown) ----
// Switching on starts boot mode, switching off starts shutdown mode
if (tog2 && !lastTog2 && toggle2State) {
// Toggle just got switched on - start boot sequence
startBootMode(); // Begin boot sequence
toggle2State = false; // Don't trigger boot again
shutdownToggle2 = true; // But prepare for shutdown when switched off
} else if (!tog2 && lastTog2 && shutdownToggle2) {
// Toggle just got switched off and we're ready for shutdown
startShutdownMode(); // Begin shutdown sequence
shutdownToggle2 = false; // Don't trigger shutdown again
toggle2State = true; // Make toggle ready for next boot
}
// ---- Handle slime mode (push button 1 while toggle 2 is on) ----
// Only works when system is idle
if (tog2 && push1 && !lastPush1 && currentState == STATE_IDLE) {
// Both toggle 2 is on AND button 1 just got pressed - start slime mode
startSlimeMode();
}
// Remember the current button states for next time
// This lets us detect when buttons change from off to on or on to off
lastPush1 = push1;
lastPush2 = push2;
lastTog1 = tog1;
lastTog2 = tog2;
lastDebounceTime = currentTime; // Remember when we last checked
}
}
// ============== STATE MANAGEMENT FUNCTIONS ==============
/**
* This function manages what mode the system is in and handles mode changes
*/
void updateSystemState(unsigned long currentTime) {
// Check what mode we're in and do the appropriate thing
switch (currentState) {
case STATE_IDLE:
// We're in idle mode - check if we should start the idle light sequence
if (digitalRead(TOGGLE_2_PIN) == LOW && digitalRead(PUSH_1_PIN) == HIGH) {
// Toggle 2 is on but button 1 is not pressed - start idle sequence
runIdleMode();
}
break;
case STATE_SLIME_MODE:
// We're in slime mode - check if the button is still being held
if (digitalRead(PUSH_1_PIN) == HIGH) {
// Button was released - automatically start the ending sequence
startSlimeEndingAuto();
} else {
// Button is still held - check if we need to loop the slime sound
if (isAudioAvailable() && !isAudioPlaying()) {
// The slime sound finished but button is still held - play it again
Serial.println("Looping track 5 while button held");
playAudioFile(AUDIO_SLIME);
}
}
break;
case STATE_SLIME_ENDING:
// We're playing the slime ending - check if it's finished
if (isAudioAvailable() && !isAudioPlaying()) {
// The ending sound finished - go back to idle
finishSlimeSequence();
} else if (!isAudioAvailable()) {
// No sound system - use a timer to simulate the ending duration
if (currentTime - modeTimer >= 2000) { // Pretend ending lasts 2 seconds
finishSlimeSequence();
}
}
break;
case STATE_SONG_PLAYING:
// We're playing music - check if the song is finished
if (isAudioAvailable() && !isAudioPlaying()) {
// Song finished - go back to idle
currentState = STATE_IDLE;
Serial.println("Song playback completed");
}
break;
default:
// Other modes (vent, boot, shutdown) are handled by the animation system
break;
}
}
/**
* This function manages all the light animations and effects
*/
void updateAnimations(unsigned long currentTime) {
// Check what animation is running and update it
switch (animState) {
case ANIM_VENT_INITIAL:
updateVentInitial(currentTime); // Handle first part of vent mode
break;
case ANIM_VENT_FLASHING:
updateVentFlashing(currentTime); // Handle flashing lights in vent mode
break;
case ANIM_VENT_ENDING:
updateVentEnding(currentTime); // Handle cleanup after vent mode
break;
case ANIM_BOOT_FLASHING:
updateBootFlashing(currentTime); // Handle flashing lights during boot
break;
case ANIM_SHUTDOWN_PROGRESSION:
updateShutdownProgression(currentTime); // Handle lights turning off during shutdown
break;
case ANIM_SLIME_FLASHING:
updateSlimeFlashing(currentTime); // Handle flashing lights during slime mode
break;
case ANIM_SLIME_ENDING:
updateSlimeEnding(currentTime); // Handle lights during slime ending
break;
case ANIM_IDLE_SEQUENCE:
updateIdleSequence(currentTime); // Handle idle light sequence
break;
default:
// No animation running - nothing to do
break;
}
}
// ============== MODE STARTING FUNCTIONS ==============
// These functions start different modes and set up their initial conditions
/**
* Start the vent mode sequence
*/
void startVentMode() {
Serial.println("Starting vent mode");
// Set the system state
currentState = STATE_VENT_MODE;
animState = ANIM_VENT_INITIAL;
modeTimer = millis(); // Remember when we started
// Set up the lights and relay
clearPowercell(); // Turn off main LED strip
setMainLEDs(false); // Turn off regular LEDs
digitalWrite(RELAY_PIN, HIGH); // Turn on relay (might control a fan or something)
// Set up sound
setVolume(LOUD_VOLUME); // Make it loud for dramatic effect
playAudioFile(AUDIO_VENT); // Play the vent sound
// Set up special lighting
setNoseLEDs(1, 5, COLOR_RED); // Make nose LEDs 1-5 red
setPowercellVentPattern(); // Set main strip to special vent pattern
}
/**
* Start the boot (startup) sequence
*/
void startBootMode() {
Serial.println("Starting boot mode");
// Set the system state
currentState = STATE_BOOT_MODE;
animState = ANIM_BOOT_FLASHING;
animationTimer = millis(); // Remember when animation started
flashCounter = 0; // Reset flash counter
// Set up sound and lights
setVolume(DEFAULT_VOLUME); // Normal volume
playAudioFile(AUDIO_BOOT); // Play boot sound
nose.setPixelColor(0, nose.Color(COLOR_GREEN)); // Make first nose LED green
setMainLEDs(true); // Turn on main LEDs
}
/**
* Start the shutdown sequence
*/
void startShutdownMode() {
Serial.println("Starting shutdown mode");
// Set the system state
currentState = STATE_SHUTDOWN_MODE;
animState = ANIM_SHUTDOWN_PROGRESSION;
modeTimer = millis(); // Remember when we started
animationStep = 0; // Start at step 0
// Set up sound
setVolume(DEFAULT_VOLUME); // Normal volume
playAudioFile(AUDIO_SHUTDOWN); // Play shutdown sound
}
/**
* Start slime mode (plays while button is held)
*/
void startSlimeMode() {
Serial.println("Starting slime mode - will play track 5 then track 6 automatically");
// Set the system state
currentState = STATE_SLIME_MODE;
animState = ANIM_SLIME_FLASHING;
flashTimer = millis(); // Remember when flashing started
modeTimer = millis(); // Remember when mode started
// Set up sound and lights
setVolume(LOUD_VOLUME); // Make it loud
playAudioFile(AUDIO_SLIME); // Play slime sound (will loop while button held)
idleCounts = max(0, idleCounts - 100); // Reduce idle counter (some kind of game mechanic)
setPowercellColor(COLOR_ORANGE); // Make main LED strip orange
}
/**
* Automatically start the slime ending sequence when button is released
*/
void startSlimeEndingAuto() {
Serial.println("Track 5 finished - automatically starting track 6 (ending)");
// Change to ending state
currentState = STATE_SLIME_ENDING;
animState = ANIM_SLIME_ENDING;
modeTimer = millis(); // Reset timer for ending duration
// Set up ending effects
digitalWrite(RELAY_PIN, HIGH); // Turn on relay
setVolume(LOUD_VOLUME); // Keep it loud
playAudioFile(AUDIO_SLIME_END); // Play ending sound
}
/**
* Finish the slime sequence and return to idle
*/
void finishSlimeSequence() {
Serial.println("Slime sequence complete - returning to idle");
// Clean up
digitalWrite(RELAY_PIN, LOW); // Turn off relay
currentState = STATE_IDLE; // Go back to idle
animState = ANIM_IDLE; // Stop animations
clearNoseLEDs(1, POWERCELL_PIXELS); // Clear any slime light effects
}
/**
* Old function kept for compatibility - just calls the new auto function
*/
void startSlimeEnd() {
startSlimeEndingAuto();
}
/**
* Start playing a music song
* @param songNumber - Which song to play (1, 2, 3, or 4)
*/
void startSongPlayback(int songNumber) {
// Calculate which audio file corresponds to this song number
int audioFile = AUDIO_SONG1 + (songNumber - 1);
// Set system state
currentState = STATE_SONG_PLAYING;
// Set up sound and lights
setVolume(DEFAULT_VOLUME); // Normal volume for music
playAudioFile(audioFile); // Play the song
setMainLEDs(true); // Turn on main LEDs
nose.setPixelColor(0, nose.Color(COLOR_BLUE)); // Make first nose LED blue
nose.show(); // Actually change the LED
setPowercellColor(COLOR_MAGENTA); // Make main strip magenta/purple
// Tell us what's happening
Serial.println("Playing song " + String(songNumber) + (isAudioAvailable() ? " with audio" : " (visual only)"));
}
/**
* Start the idle mode sequence
*/
void runIdleMode() {
// Only start if we're not already running idle sequence
if (animState == ANIM_IDLE) {
animState = ANIM_IDLE_SEQUENCE; // Start the idle animation
animationTimer = millis(); // Remember when we started
animationStep = 0; // Start at step 0
// Set up sound and lights
setVolume(DEFAULT_VOLUME); // Normal volume
playAudioFile(AUDIO_IDLE); // Play idle/ambient sound
setMainLEDs(true); // Turn on main LEDs
clearPowercell(); // Clear main LED strip
idleCounts = 500; // Reset idle counter
}
}
// ============== ANIMATION UPDATE FUNCTIONS ==============
// These functions handle the step-by-step updates for each animation
/**
* Handle the initial waiting period of vent mode
*/
void updateVentInitial(unsigned long currentTime) {
// Check if enough time has passed for the initial delay
if (currentTime - modeTimer >= VENT_INITIAL_DELAY) {
// Time's up - move to the flashing phase
animState = ANIM_VENT_FLASHING;
animationTimer = currentTime; // Start timing the flashing
flashCounter = 0; // Reset flash counter
flashState = false; // Start with lights off
}
}
/**
* Handle the flashing lights during vent mode
*/
void updateVentFlashing(unsigned long currentTime) {
// Check if it's time for the next flash
if (currentTime - animationTimer >= VENT_FLASH_DELAY) {
flashState = !flashState; // Toggle lights on/off
setMainLEDs(flashState); // Actually change the lights
animationTimer = currentTime; // Reset timer
// Count flashes when lights turn off
if (!flashState) {
flashCounter++;
// Check if we've flashed enough times
if (flashCounter >= VENT_FLASH_COUNT) {
// Done flashing - move to ending phase
animState = ANIM_VENT_ENDING;
modeTimer = currentTime; // Start timing the ending
setMainLEDs(false); // Make sure lights are off
}
}
}
}
/**
* Handle the cleanup phase after vent mode
*/
void updateVentEnding(unsigned long currentTime) {
// Check if enough time has passed for the ending delay
if (currentTime - modeTimer >= VENT_END_DELAY) {
// Time's up - clean up and return to idle
clearNoseLEDs(1, 5); // Turn off nose LEDs that were red
digitalWrite(RELAY_PIN, LOW); // Turn off relay
currentState = STATE_IDLE; // Return to idle state
animState = ANIM_IDLE; // Stop animations
}
}
/**
* Handle the flashing lights during boot mode
*/
void updateBootFlashing(unsigned long currentTime) {
// Check if it's time for the next flash
if (currentTime - animationTimer >= BOOT_FLASH_DELAY) {
flashState = !flashState; // Toggle flash state
digitalWrite(LED_PIN_6, flashState ? HIGH : LOW); // Flash LED 6
nose.show(); // Update nose LEDs
animationTimer = currentTime; // Reset timer
// Count flashes when lights turn off
if (!flashState) {
flashCounter++;
// Check if we've flashed enough times
if (flashCounter >= BOOT_FLASH_COUNT) {
// Done with boot sequence - return to idle
currentState = STATE_IDLE;
animState = ANIM_IDLE;
}
}
}
}
/**
* Handle the progressive LED shutdown animation
*/
void updateShutdownProgression(unsigned long currentTime) {
// First, wait for the initial delay
if (currentTime - modeTimer >= SHUTDOWN_DELAY && animationStep == 0) {
animationStep = 1; // Start the LED progression
animationTimer = currentTime; // Start timing LED changes
}
// Then turn off LEDs one by one
if (animationStep > 0 && currentTime - animationTimer >= SHUTDOWN_LED_DELAY) {
if (animationStep <= POWERCELL_PIXELS) {
// Turn off the next LED in the strip
powercell.setPixelColor(animationStep - 1, powercell.Color(COLOR_OFF));
powercell.show(); // Actually update the LED strip
animationStep++; // Move to next LED
animationTimer = currentTime; // Reset timer
} else {
// All LEDs are off - shutdown complete
setMainLEDs(false); // Turn off main LEDs too
currentState = STATE_IDLE; // Return to idle
animState = ANIM_IDLE; // Stop animations
}
}
}
/**
* Handle the flashing lights during slime mode
*/
void updateSlimeFlashing(unsigned long currentTime) {
// Check if it's time to toggle the flash
if (currentTime - flashTimer >= IDLE_FLASH_DELAY) {
flashState = !flashState; // Toggle flash state
if (flashState) {
// Lights on - make them pink
setNoseLEDs(1, POWERCELL_PIXELS, COLOR_PINK);
} else {
// Lights off - turn them off
clearNoseLEDs(1, POWERCELL_PIXELS);
}
flashTimer = currentTime; // Reset timer
}
}
/**
* Handle the lights during slime ending sequence
*/
void updateSlimeEnding(unsigned long currentTime) {
// Continue the flashing effect during the ending sound
// (The actual ending logic is handled in updateSystemState)
if (currentTime - flashTimer >= IDLE_FLASH_DELAY) {
flashState = !flashState; // Toggle flash state
if (flashState) {
// Lights on - make them pink
setNoseLEDs(1, POWERCELL_PIXELS, COLOR_PINK);
} else {
// Lights off - turn them off
clearNoseLEDs(1, POWERCELL_PIXELS);
}
flashTimer = currentTime; // Reset timer
}
}
/**
* Handle the progressive light sequence during idle mode
*/
void updateIdleSequence(unsigned long currentTime) {
// Check if it's time for the next step
if (currentTime - animationTimer >= IDLE_SEQUENCE_DELAY) {
if (animationStep < POWERCELL_PIXELS) {
// Turn on the next LED in orange
powercell.setPixelColor(animationStep, powercell.Color(COLOR_ORANGE));
powercell.show(); // Actually update the LED strip
digitalWrite(LED_PIN_6, LOW); // Turn off LED 6
animationStep++; // Move to next LED
animationTimer = currentTime; // Reset timer
} else {
// All LEDs are on - sequence complete
animState = ANIM_IDLE; // Stop the sequence
}
}
}
// ============== LED HELPER FUNCTIONS ==============
// These functions make it easier to control the lights
/**
* Turn the main LEDs on or off
* @param state - true = on, false = off
*/
void setMainLEDs(bool state) {
digitalWrite(LED_PIN_6, state ? HIGH : LOW);
digitalWrite(LED_PIN_7, state ? HIGH : LOW);
digitalWrite(LED_PIN_8, state ? HIGH : LOW);
}
/**
* Turn off all LEDs in the main LED strip
*/
void clearPowercell() {
for (int i = 0; i < POWERCELL_PIXELS; i++) {
powercell.setPixelColor(i, powercell.Color(COLOR_OFF));
}
powercell.show(); // Actually update the LED strip
}
/**
* Set all LEDs in the main strip to the same color
* @param r - red amount (0-255)
* @param g - green amount (0-255)
* @param b - blue amount (0-255)
*/
void setPowercellColor(uint8_t r, uint8_t g, uint8_t b) {
for (int i = 0; i < POWERCELL_PIXELS; i++) {
powercell.setPixelColor(i, powercell.Color(r, g, b));
}
powercell.show(); // Actually update the LED strip
}
/**
* Set the main LED strip to a special pattern for vent mode
*/
void setPowercellVentPattern() {
powercell.setPixelColor(0, powercell.Color(COLOR_MAGENTA)); // LED 0 = purple
powercell.setPixelColor(1, powercell.Color(COLOR_MAGENTA)); // LED 1 = purple
powercell.setPixelColor(2, powercell.Color(COLOR_MAGENTA)); // LED 2 = purple
powercell.setPixelColor(3, powercell.Color(COLOR_WHITE)); // LED 3 = white
powercell.setPixelColor(4, powercell.Color(COLOR_WHITE)); // LED 4 = white
powercell.setPixelColor(5, powercell.Color(COLOR_MAGENTA)); // LED 5 = purple
powercell.setPixelColor(6, powercell.Color(COLOR_MAGENTA)); // LED 6 = purple
powercell.show(); // Actually update the LED strip
}
/**
* Set a range of nose LEDs to a specific color
* @param start - first LED number to change
* @param end - last LED number to change
* @param r - red amount (0-255)
* @param g - green amount (0-255)
* @param b - blue amount (0-255)
*/
void setNoseLEDs(int start, int end, uint8_t r, uint8_t g, uint8_t b) {
for (int i = start; i <= end; i++) {
nose.setPixelColor(i, nose.Color(r, g, b));
}
nose.show(); // Actually update the LED strip
}
/**
* Turn off a range of nose LEDs
* @param start - first LED number to turn off
* @param end - last LED number to turn off
*/
void clearNoseLEDs(int start, int end) {
setNoseLEDs(start, end, COLOR_OFF); // Just call setNoseLEDs with "off" color
}