/*
Proton Pack – Afterlife Edition - Movie-Realistic with Manual Vent Control
All timing uses millis() - no delay() calls anywhere!
─────────────────────────────────────────────────────────────
🆕 MANUAL VENT SYSTEM (NEW!):
Manual Vent Button (PIN 3) - Press and hold for variable venting:
- Only works when system is powered up (State 2+)
- Short tap (<1.5s): Brief cyclotron slowdown + quick recovery
- Long hold (>1.5s): Triggers full automatic vent sequence
- Progressive response: Longer hold = more severe slowdown
- Visual feedback: Power cell shows stress indicators
- Proportional recovery: Recovery time = 50% of hold time
🔇 OFF STATE FEEDBACK:
- Fire button when OFF: Plays mechanical click sound
- Vent button when OFF: Ignored with debug message
─────────────────────────────────────────────────────────────
PINS:
TOGGLE 1 → PIN 5 = Master Power On
TOGGLE 2 → PIN 6 = Ready Burst Mode
FIRE BTN → PIN 2 = Fire trigger
MANUAL VENT → PIN 3 = Manual vent control (NEW!)
BUZZER → PIN 4 = Sound effects (moved from PIN 3)
NEOPIXELS:
POWER_CELL = PIN 7, 10 LEDs
CYCLOTRON = PIN 8, 20 LEDs
SMOKE_PIN = PIN 9 (MOSFET or LED sim for vent)
📝 NOTE: For MP3 click sounds, add DFPlayer Mini module
Current code uses tone() for basic click effect
*/
#include <Adafruit_NeoPixel.h>
#define FIRE_BUTTON 2
#define MANUAL_VENT_BTN 3 // New manual vent button
#define BUZZER_PIN 4 // Moved buzzer to pin 4
#define TOGGLE_MASTER 5
#define TOGGLE_READY 6
#define POWER_CELL_PIN 7
#define CYCLOTRON_PIN 8
#define SMOKE_PIN 9
#define POWER_CELL_LEDS 10
#define CYCLOTRON_LEDS 20
#define LED_BRIGHTNESS 180
#define FIRE_DURATION 2000
#define VENT_DURATION 3000
// Speed control for boot sequence (adjustable)
#define BOOT_SEQUENCE_DURATION 3000 // Can be changed to speed up/slow down boot
// Cyclotron speed limits (in milliseconds between updates)
#define CYCLOTRON_SPEED_MIN 30 // Fastest speed (during fire)
#define CYCLOTRON_SPEED_MAX 400 // Slowest speed (during boot start)
#define CYCLOTRON_SPEED_IDLE 150 // Normal idle speed
#define CYCLOTRON_SPEED_READY 80 // Ready state speed
#define CYCLOTRON_SPEED_VENT 800 // Very slow during venting (almost stopped)
#define CYCLOTRON_SPEED_CRITICAL 1200 // Critical slow during peak vent
// Manual vent settings
#define MANUAL_VENT_THRESHOLD 1500 // 1.5 seconds to trigger full vent
Adafruit_NeoPixel powerCell(POWER_CELL_LEDS, POWER_CELL_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel cyclotron(CYCLOTRON_LEDS, CYCLOTRON_PIN, NEO_GRB + NEO_KHZ800);
enum State : uint8_t {
STATE_OFF = 0,
STATE_BOOT,
STATE_IDLE,
STATE_READY_BURST,
STATE_READY_FULL,
STATE_VENT
};
enum SoundState : uint8_t {
SOUND_OFF = 0,
SOUND_BOOT,
SOUND_FIRE,
SOUND_VENT,
SOUND_CLICK // New click sound for off state
};
// Main state variables
State currentState = STATE_OFF;
unsigned long stateStart = 0;
unsigned long fireStart = 0;
// Animation timing variables
unsigned long lastPowerCellUpdate = 0;
unsigned long lastCyclotronUpdate = 0;
unsigned long lastFireFlicker = 0;
uint8_t powerCellPulse = 30;
int8_t powerCellDirection = 1;
uint8_t cyclotronStep = 0;
// Sound system variables
SoundState soundState = SOUND_OFF;
unsigned long soundStart = 0;
uint8_t soundStep = 0;
bool soundEnabled = true;
// Button debouncing
bool lastFireBtn = false;
unsigned long lastFireTime = 0;
bool firePressed = false;
bool lastMasterToggle = false;
bool lastReadyToggle = false;
unsigned long lastToggleTime = 0;
// Realism flags
bool justVented = false;
unsigned long ventCompleteTime = 0;
unsigned long spinupStartTime = 0;
// Manual vent system
bool manualVentActive = false;
unsigned long manualVentStartTime = 0;
unsigned long manualVentDuration = 0;
bool manualVentRecovery = false;
unsigned long manualVentRecoveryStart = 0;
// Troubleshooting flags
bool ignoreToggleBounce = false; // Set to true to ignore bouncing toggle
void setState(State s) {
State previousState = currentState;
currentState = s;
stateStart = millis();
Serial.print("Switched to state: ");
Serial.println(s);
// Handle realistic state transitions
if (previousState == STATE_VENT && s == STATE_IDLE) {
justVented = true;
ventCompleteTime = millis();
spinupStartTime = millis();
Serial.println("System recovering from vent - spinning up...");
} else {
justVented = false;
}
// Trigger sound effects
if (soundEnabled) {
switch(s) {
case STATE_BOOT:
startSound(SOUND_BOOT);
break;
case STATE_READY_FULL:
startSound(SOUND_FIRE);
break;
case STATE_VENT:
startSound(SOUND_VENT);
break;
default:
stopSound();
break;
}
}
// Handle smoke/vent output
if (s == STATE_VENT) digitalWrite(SMOKE_PIN, HIGH);
else digitalWrite(SMOKE_PIN, LOW);
}
void startSound(SoundState s) {
soundState = s;
soundStart = millis();
soundStep = 0;
}
void stopSound() {
soundState = SOUND_OFF;
noTone(BUZZER_PIN);
}
void setup() {
Serial.begin(115200);
pinMode(FIRE_BUTTON, INPUT_PULLUP);
pinMode(MANUAL_VENT_BTN, INPUT_PULLUP);
pinMode(TOGGLE_MASTER, INPUT_PULLUP);
pinMode(TOGGLE_READY, INPUT_PULLUP);
pinMode(SMOKE_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
powerCell.begin();
cyclotron.begin();
powerCell.setBrightness(LED_BRIGHTNESS);
cyclotron.setBrightness(LED_BRIGHTNESS);
powerCell.clear();
cyclotron.clear();
powerCell.show();
cyclotron.show();
setState(STATE_OFF);
Serial.println("Proton Pack initialized - Who ya gonna call?");
Serial.print("Boot sequence duration: ");
Serial.print(BOOT_SEQUENCE_DURATION);
Serial.println("ms");
Serial.println("Type 'status' for info or 'speed####' to change boot speed");
Serial.println("NEW: Manual vent button on PIN 3 - hold for variable venting!");
Serial.println();
Serial.println("=== TROUBLESHOOTING ===");
Serial.println("If stuck bouncing between states, check:");
Serial.println("1. Toggle switch wiring (not momentary button)");
Serial.println("2. Solid connections (no loose wires)");
Serial.println("3. Pull-up resistors if needed");
Serial.println("4. Type 'pins' to check pin states");
Serial.println("========================");
}
void loop() {
// Read inputs with debouncing
readInputs();
// Check for serial commands for speed adjustment
checkSerialCommands();
// Update main state machine
updateStateMachine();
// Update animations
updatePowerCell();
updateCyclotron();
// Update sound effects
updateSound();
}
void readInputs() {
unsigned long now = millis();
// Read current states
bool masterOn = !digitalRead(TOGGLE_MASTER);
bool readyOn = !digitalRead(TOGGLE_READY);
bool fireBtn = !digitalRead(FIRE_BUTTON);
bool ventBtn = !digitalRead(MANUAL_VENT_BTN);
// Handle manual vent button - ONLY works in state 2+ (powered up)
static bool lastVentBtn = false;
if (currentState >= STATE_IDLE) { // Only allow manual vent when system is powered up
if (ventBtn && !lastVentBtn) {
// Button just pressed - start manual vent
manualVentActive = true;
manualVentStartTime = now;
Serial.println("Manual vent initiated...");
} else if (!ventBtn && lastVentBtn) {
// Button just released - end manual vent and start recovery
if (manualVentActive) {
manualVentDuration = now - manualVentStartTime;
manualVentActive = false;
Serial.print("Manual vent released after ");
Serial.print(manualVentDuration);
Serial.println("ms");
// Check if we held long enough to trigger full auto-vent
if (manualVentDuration >= MANUAL_VENT_THRESHOLD && currentState != STATE_VENT) {
Serial.println("Long vent detected - triggering full vent sequence!");
setState(STATE_VENT);
} else {
// Short vent - start recovery sequence
manualVentRecovery = true;
manualVentRecoveryStart = now;
Serial.println("Short vent - beginning recovery");
}
}
}
} else {
// System not powered up - ignore vent button but provide feedback
if (ventBtn && !lastVentBtn) {
Serial.println("Manual vent ignored - system not powered up");
}
}
lastVentBtn = ventBtn;
// Debounce fire button - detect press edge only
firePressed = false;
if (fireBtn && !lastFireBtn && (now - lastFireTime > 100)) {
firePressed = true;
lastFireTime = now;
// Special handling for fire button when system is OFF
if (currentState == STATE_OFF) {
Serial.println("Fire button pressed while OFF - playing click sound");
startSound(SOUND_CLICK);
} else {
Serial.println("Fire button pressed!");
}
}
lastFireBtn = fireBtn;
// Enhanced toggle debouncing with debug output
static bool lastMasterRaw = false;
static bool lastReadyRaw = false;
static unsigned long masterStableTime = 0;
static unsigned long readyStableTime = 0;
static unsigned long masterConfirmTime = 0;
static unsigned long readyConfirmTime = 0;
// Enhanced toggle debouncing with debug output
static bool lastMasterRaw = false;
static bool lastReadyRaw = false;
static unsigned long masterStableTime = 0;
static unsigned long readyStableTime = 0;
static unsigned long masterConfirmTime = 0;
static unsigned long readyConfirmTime = 0;
if (ignoreToggleBounce) {
// Emergency mode - ignore debouncing, respond immediately
if (now - lastToggleTime > 50) { // Minimal delay
if (masterOn != lastMasterToggle) {
lastMasterToggle = masterOn;
Serial.print("Master toggle IMMEDIATE: ");
Serial.println(masterOn ? "ON" : "OFF");
}
if (readyOn != lastReadyToggle) {
lastReadyToggle = readyOn;
Serial.print("Ready toggle IMMEDIATE: ");
Serial.println(readyOn ? "ON" : "OFF");
}
lastToggleTime = now;
}
} else {
// Normal debouncing - require 300ms stability
if (now - lastToggleTime > 200) { // Check every 200ms
// Master toggle stability check
if (masterOn != lastMasterRaw) {
lastMasterRaw = masterOn;
masterStableTime = now;
masterConfirmTime = 0; // Reset confirmation timer
// Debug output for toggle changes
Serial.print("Master toggle bounce detected: ");
Serial.println(masterOn ? "ON" : "OFF");
} else if (now - masterStableTime > 300) { // Stable for 300ms
if (masterConfirmTime == 0) {
masterConfirmTime = now; // Start confirmation timer
} else if (now - masterConfirmTime > 100) { // Confirmed for additional 100ms
if (masterOn != lastMasterToggle) {
lastMasterToggle = masterOn;
Serial.print("Master toggle CONFIRMED: ");
Serial.println(masterOn ? "ON" : "OFF");
}
}
}
// Ready toggle stability check
if (readyOn != lastReadyRaw) {
lastReadyRaw = readyOn;
readyStableTime = now;
readyConfirmTime = 0;
} else if (now - readyStableTime > 300) { // Stable for 300ms
if (readyConfirmTime == 0) {
readyConfirmTime = now;
} else if (now - readyConfirmTime > 100) { // Confirmed for additional 100ms
if (readyOn != lastReadyToggle) {
lastReadyToggle = readyOn;
Serial.print("Ready toggle CONFIRMED: ");
Serial.println(readyOn ? "ON" : "OFF");
}
}
}
lastToggleTime = now;
}
}
}
void updateStateMachine() {
bool masterOn = lastMasterToggle;
bool readyOn = lastReadyToggle;
// Add minimum time requirements between state changes to prevent bounce
static unsigned long lastStateChange = 0;
unsigned long now = millis();
bool canChangeState = (now - lastStateChange > 500); // Minimum 500ms between state changes
switch (currentState) {
case STATE_OFF:
if (masterOn && canChangeState) {
setState(STATE_BOOT);
lastStateChange = now;
}
break;
case STATE_BOOT:
if (!masterOn && canChangeState) {
setState(STATE_OFF);
lastStateChange = now;
}
else if (millis() - stateStart > BOOT_SEQUENCE_DURATION && canChangeState) {
setState(STATE_IDLE);
lastStateChange = now;
}
break;
case STATE_IDLE:
if (!masterOn && canChangeState) {
setState(STATE_OFF);
lastStateChange = now;
}
else if (readyOn && canChangeState) {
setState(STATE_READY_BURST);
lastStateChange = now;
}
break;
case STATE_READY_BURST:
if (!masterOn && canChangeState) {
setState(STATE_OFF);
lastStateChange = now;
}
else if (!readyOn && canChangeState) {
setState(STATE_IDLE);
lastStateChange = now;
}
else if (firePressed) {
fireStart = millis();
setState(STATE_READY_FULL);
lastStateChange = now;
}
break;
case STATE_READY_FULL:
if (!masterOn && canChangeState) {
setState(STATE_OFF);
lastStateChange = now;
}
else if (millis() - fireStart >= FIRE_DURATION) {
setState(STATE_VENT);
lastStateChange = now;
}
break;
case STATE_VENT:
if (!masterOn && canChangeState) {
setState(STATE_OFF);
lastStateChange = now;
}
else if (millis() - stateStart >= VENT_DURATION) {
Serial.println("Venting complete - beginning recovery sequence");
setState(STATE_IDLE);
lastStateChange = now;
} else {
// Debug output for venting phases
static unsigned long lastVentDebug = 0;
if (millis() - lastVentDebug > 1000) {
unsigned long elapsed = millis() - stateStart;
float ventProgress = (float)elapsed / (float)VENT_DURATION;
if (ventProgress < 0.3) {
Serial.println("VENT: Critical overload phase");
} else if (ventProgress < 0.8) {
Serial.println("VENT: Deep venting phase - system minimal");
} else {
Serial.println("VENT: Recovery preparation");
}
lastVentDebug = millis();
}
}
break;
}
// Clear fire button press after processing
firePressed = false;
}
void updatePowerCell() {
unsigned long now = millis();
// Check for manual vent override first
if (manualVentActive || manualVentRecovery) {
if (now - lastPowerCellUpdate > 30) {
manualVentPowerCellAnimation();
lastPowerCellUpdate = now;
}
return;
}
switch (currentState) {
case STATE_OFF:
if (now - lastPowerCellUpdate > 50) {
powerCell.clear();
powerCell.show();
lastPowerCellUpdate = now;
}
break;
case STATE_BOOT:
if (now - lastPowerCellUpdate > 30) {
powerUpAnimation();
lastPowerCellUpdate = now;
}
break;
case STATE_IDLE:
if (now - lastPowerCellUpdate > 20) {
idleAnimation();
lastPowerCellUpdate = now;
}
break;
case STATE_READY_BURST:
if (now - lastPowerCellUpdate > 15) {
readyAnimation();
lastPowerCellUpdate = now;
}
break;
case STATE_READY_FULL:
if (now - lastFireFlicker > 30) {
fireAnimation();
lastFireFlicker = now;
}
break;
case STATE_VENT:
if (now - lastPowerCellUpdate > 40) {
ventAnimation();
lastPowerCellUpdate = now;
}
break;
}
}
void updateCyclotron() {
unsigned long now = millis();
uint16_t speed;
// Check for manual vent override first
if (manualVentActive) {
// Progressive slowdown based on how long button is held
unsigned long holdTime = now - manualVentStartTime;
float slowdownFactor = (float)holdTime / (float)MANUAL_VENT_THRESHOLD;
if (slowdownFactor > 1.0) slowdownFactor = 1.0;
// Calculate manual vent speed (normal speed + progressive slowdown)
uint16_t baseSpeed = CYCLOTRON_SPEED_IDLE;
uint16_t maxSlowdown = CYCLOTRON_SPEED_CRITICAL - baseSpeed;
speed = baseSpeed + (uint16_t)(slowdownFactor * maxSlowdown);
// Add stuttering effect as it gets worse
if (slowdownFactor > 0.7 && random(0, 100) < 20) {
speed += random(0, 300);
}
} else if (manualVentRecovery) {
// Recovery from manual vent
unsigned long recoveryTime = now - manualVentRecoveryStart;
float recoveryDuration = manualVentDuration * 0.5; // Recovery takes 50% of vent time
if (recoveryTime < recoveryDuration) {
float recoveryProgress = (float)recoveryTime / recoveryDuration;
// Spin up from slow back to normal
uint16_t maxRecoverySpeed = CYCLOTRON_SPEED_CRITICAL;
uint16_t normalSpeed = CYCLOTRON_SPEED_IDLE;
speed = maxRecoverySpeed - (uint16_t)(recoveryProgress * (maxRecoverySpeed - normalSpeed));
} else {
// Recovery complete
manualVentRecovery = false;
speed = CYCLOTRON_SPEED_IDLE;
Serial.println("Manual vent recovery complete");
}
} else {
// Normal state-based speed determination
switch(currentState) {
case STATE_OFF:
if (now - lastCyclotronUpdate > 50) {
cyclotron.clear();
cyclotron.show();
lastCyclotronUpdate = now;
}
return;
case STATE_BOOT: {
// Dynamic speed that matches power cell fill progress
unsigned long elapsed = millis() - stateStart;
float progress = (float)elapsed / (float)BOOT_SEQUENCE_DURATION;
if (progress > 1.0) progress = 1.0;
// Speed increases as power cell fills (slow to fast)
speed = CYCLOTRON_SPEED_MAX - (uint16_t)(progress * (CYCLOTRON_SPEED_MAX - CYCLOTRON_SPEED_IDLE));
break;
}
case STATE_IDLE: {
// Check if we just came from venting - need spinup sequence
if (justVented) {
unsigned long elapsed = millis() - spinupStartTime;
float spinupDuration = BOOT_SEQUENCE_DURATION * 0.7; // 70% of boot time
if (elapsed < spinupDuration) {
// Gradually spin up from very slow to normal idle speed
float progress = (float)elapsed / spinupDuration;
speed = CYCLOTRON_SPEED_CRITICAL - (uint16_t)(progress * (CYCLOTRON_SPEED_CRITICAL - CYCLOTRON_SPEED_IDLE));
} else {
// Spinup complete, return to normal idle
justVented = false;
speed = CYCLOTRON_SPEED_IDLE;
Serial.println("Spinup complete - system nominal");
}
} else {
speed = CYCLOTRON_SPEED_IDLE;
}
break;
}
case STATE_READY_BURST:
speed = CYCLOTRON_SPEED_READY;
break;
case STATE_READY_FULL:
speed = CYCLOTRON_SPEED_MIN; // Maximum speed during firing
break;
case STATE_VENT: {
// Realistic venting - cyclotron slows to almost a stop
unsigned long elapsed = millis() - stateStart;
float ventProgress = (float)elapsed / (float)VENT_DURATION;
if (ventProgress < 0.3) {
// First 30% - rapid slowdown
float slowdown = ventProgress / 0.3;
speed = CYCLOTRON_SPEED_READY + (uint16_t)(slowdown * (CYCLOTRON_SPEED_CRITICAL - CYCLOTRON_SPEED_READY));
} else if (ventProgress < 0.8) {
// Middle 50% - very slow, almost stopped
speed = CYCLOTRON_SPEED_CRITICAL;
// Random stuttering during critical phase
if (random(0, 100) < 20) {
speed = CYCLOTRON_SPEED_CRITICAL + random(0, 400);
}
} else {
// Last 20% - slight recovery
float recovery = (ventProgress - 0.8) / 0.2;
speed = CYCLOTRON_SPEED_CRITICAL - (uint16_t)(recovery * 200);
}
break;
}
default:
speed = CYCLOTRON_SPEED_IDLE;
break;
}
}
if (now - lastCyclotronUpdate >= speed) {
spinCyclotron();
lastCyclotronUpdate = now;
}
}
void powerUpAnimation() {
unsigned long elapsed = millis() - stateStart;
// Calculate progress using adjustable boot duration
float progress = (float)elapsed / (float)BOOT_SEQUENCE_DURATION;
if (progress > 1.0) progress = 1.0;
// Calculate drop position (travels from LED 9 down to LED 0)
float dropPosition = 9.0 - (progress * 9.0);
int dropLED = (int)dropPosition;
// Calculate how many LEDs should be filled from bottom up
int fillLevel = (int)(progress * POWER_CELL_LEDS);
// Running animation offset for filled pixels
static unsigned long lastRunUpdate = 0;
static uint8_t runOffset = 0;
if (millis() - lastRunUpdate > 100) { // Running animation speed
runOffset = (runOffset + 1) % 3;
lastRunUpdate = millis();
}
// Clear all LEDs first
powerCell.clear();
// Fill from bottom up with running animation
for (int i = 0; i < fillLevel; i++) {
uint8_t brightness = 180;
// Add running pulse effect to filled LEDs
if ((i + runOffset) % 3 == 0) {
brightness = 220; // Brighter pulse
}
powerCell.setPixelColor(i, powerCell.Color(0, 0, brightness));
}
// Add the bright "drop" pixel traveling downward
if (dropLED >= 0 && dropLED < POWER_CELL_LEDS && progress < 1.0) {
powerCell.setPixelColor(dropLED, powerCell.Color(0, 150, 255)); // Bright cyan drop
// Add trailing effects
if (dropLED < 9) {
powerCell.setPixelColor(dropLED + 1, powerCell.Color(0, 80, 200)); // Trail 1
}
if (dropLED < 8) {
powerCell.setPixelColor(dropLED + 2, powerCell.Color(0, 40, 150)); // Trail 2
}
}
powerCell.show();
}
void idleAnimation() {
// Check if we're in post-vent spinup mode
if (justVented) {
postVentRecoveryAnimation();
return;
}
// Charging animation - energy slowly fills from bottom to top
static unsigned long lastChargeUpdate = 0;
static float chargeLevel = 0.0;
static bool chargingUp = true;
if (millis() - lastChargeUpdate > 25) { // Smooth 40fps update
if (chargingUp) {
chargeLevel += 0.008; // Slow charge rate
if (chargeLevel >= 1.0) {
chargeLevel = 1.0;
chargingUp = false;
}
} else {
chargeLevel -= 0.012; // Slightly faster discharge
if (chargeLevel <= 0.0) {
chargeLevel = 0.0;
chargingUp = true;
}
}
// Clear all LEDs
powerCell.clear();
// Calculate how many LEDs should be fully charged
float exactLevel = chargeLevel * POWER_CELL_LEDS;
int fullLEDs = (int)exactLevel;
float partialBrightness = exactLevel - fullLEDs;
// Light up the fully charged LEDs (from bottom up)
for (int i = 0; i < fullLEDs; i++) {
uint8_t baseBrightness = 120;
// Add subtle energy fluctuation to each LED
uint8_t fluctuation = random(0, 30);
uint8_t finalBrightness = baseBrightness + fluctuation;
// Slightly brighter blue for higher LEDs (more "charged")
uint8_t blueTint = map(i, 0, POWER_CELL_LEDS-1, 100, 150);
powerCell.setPixelColor(i, powerCell.Color(0, 0, min(255, (int)finalBrightness + (blueTint - 100))));
}
// Add the partially charging LED with fade effect
if (fullLEDs < POWER_CELL_LEDS && partialBrightness > 0) {
uint8_t partialLevel = (uint8_t)(partialBrightness * 120);
// Add charging "sparkle" effect to the leading edge
if (random(0, 100) < 40) {
partialLevel += random(20, 60); // Random energy burst
}
powerCell.setPixelColor(fullLEDs, powerCell.Color(0, 0, partialLevel));
}
// Add occasional energy "bubbles" - random LEDs briefly lighting
if (random(0, 100) < 8) { // 8% chance
int bubbleLED = random(fullLEDs + 1, POWER_CELL_LEDS);
uint8_t bubbleBrightness = random(30, 80);
powerCell.setPixelColor(bubbleLED, powerCell.Color(0, bubbleBrightness/3, bubbleBrightness));
}
powerCell.show();
lastChargeUpdate = millis();
}
}
void postVentRecoveryAnimation() {
unsigned long elapsed = millis() - spinupStartTime;
float spinupDuration = BOOT_SEQUENCE_DURATION * 0.7;
if (elapsed < spinupDuration) {
float progress = (float)elapsed / spinupDuration;
// Start dim and gradually brighten
uint8_t baseBrightness = (uint8_t)(progress * 100);
// Add some flickering during early recovery
uint8_t flicker = baseBrightness;
if (progress < 0.5 && random(0, 100) < 30) {
flicker = baseBrightness / 2;
}
for (int i = 0; i < POWER_CELL_LEDS; i++) {
// Some LEDs recover faster than others for realism
uint8_t ledBrightness = flicker;
if (i < progress * POWER_CELL_LEDS) {
ledBrightness = flicker + 20; // Slightly brighter for recovered LEDs
}
powerCell.setPixelColor(i, powerCell.Color(0, 0, ledBrightness));
}
powerCell.show();
}
}
void readyAnimation() {
static unsigned long lastReadyUpdate = 0;
static float fillLevel = 0.0;
static bool building = true;
if (millis() - lastReadyUpdate > 15) { // Fast update for responsive feel
if (building) {
fillLevel += 0.025; // Fill speed - adjust for faster/slower charging
if (fillLevel >= 1.0) {
fillLevel = 1.0;
building = false;
// Brief pause at full charge
static unsigned long fullChargeTime = 0;
if (fullChargeTime == 0) fullChargeTime = millis();
if (millis() - fullChargeTime > 200) { // 200ms pause at full
building = true;
fillLevel = 0.0;
fullChargeTime = 0;
}
}
}
// Clear all LEDs
powerCell.clear();
// Calculate fill level
float exactLevel = fillLevel * POWER_CELL_LEDS;
int fullLEDs = (int)exactLevel;
float partialBrightness = exactLevel - fullLEDs;
// Fill the bar from bottom up with ready colors (blue-white energy)
for (int i = 0; i < fullLEDs; i++) {
uint8_t intensity = 200;
// Higher LEDs get progressively brighter (building pressure)
uint8_t boost = map(i, 0, POWER_CELL_LEDS-1, 0, 55);
// Ready state colors - bright blue with white energy
uint8_t red = 0;
uint8_t green = 50 + boost/2; // Slight cyan tint
uint8_t blue = intensity + boost;
powerCell.setPixelColor(i, powerCell.Color(red, green, min(255, (int)blue)));
}
// Add the filling LED with bright edge effect
if (fullLEDs < POWER_CELL_LEDS && partialBrightness > 0) {
uint8_t edgeIntensity = (uint8_t)(partialBrightness * 255);
// Bright white edge for the "charging front"
powerCell.setPixelColor(fullLEDs, powerCell.Color(edgeIntensity/3, edgeIntensity/2, edgeIntensity));
}
// Add energy overflow effect when near full
if (fillLevel > 0.8) {
// Random bright pixels above fill level (energy crackling)
if (random(0, 100) < 30) {
int sparkLED = random(fullLEDs, POWER_CELL_LEDS);
uint8_t sparkIntensity = random(100, 255);
powerCell.setPixelColor(sparkLED, powerCell.Color(sparkIntensity/4, sparkIntensity/2, sparkIntensity));
}
}
powerCell.show();
lastReadyUpdate = millis();
}
}
void fireAnimation() {
uint32_t r = random(200, 255);
uint32_t g = random(100, 200);
uint32_t b = random(20, 50);
for (int i = 0; i < POWER_CELL_LEDS; i++) {
powerCell.setPixelColor(i, powerCell.Color(r, g, b));
}
powerCell.show();
}
void ventAnimation() {
unsigned long elapsed = millis() - stateStart;
float ventProgress = (float)elapsed / (float)VENT_DURATION;
// Realistic venting phases
if (ventProgress < 0.2) {
// Phase 1: Immediate overload - bright flashing
uint8_t intensity = random(150, 255);
for (int i = 0; i < POWER_CELL_LEDS; i++) {
if (random(0, 100) < 70) {
powerCell.setPixelColor(i, powerCell.Color(intensity, intensity/4, 0)); // Orange warning
} else {
powerCell.setPixelColor(i, powerCell.Color(intensity/2, 0, 0)); // Red critical
}
}
} else if (ventProgress < 0.6) {
// Phase 2: Critical venting - dimming with random surges
uint8_t baseFade = map(elapsed, VENT_DURATION * 0.2, VENT_DURATION * 0.6, 200, 50);
for (int i = 0; i < POWER_CELL_LEDS; i++) {
uint8_t ledBrightness = baseFade;
// Random surges on individual LEDs
if (random(0, 100) < 15) {
ledBrightness = random(100, 200);
}
// Some LEDs might go completely dark (system stress)
if (random(0, 100) < 10) {
ledBrightness = 0;
}
powerCell.setPixelColor(i, powerCell.Color(ledBrightness/3, 0, ledBrightness/2));
}
} else {
// Phase 3: Recovery preparation - slow stabilization
uint8_t fade = map(elapsed, VENT_DURATION * 0.6, VENT_DURATION, 50, 20);
// Gradual stabilization
for (int i = 0; i < POWER_CELL_LEDS; i++) {
uint8_t stability = fade;
if (random(0, 100) < 80) { // 80% chance of stable dim glow
powerCell.setPixelColor(i, powerCell.Color(0, 0, stability));
} else { // 20% chance of flicker
powerCell.setPixelColor(i, powerCell.Color(0, 0, stability/2));
}
}
}
powerCell.show();
}
void manualVentPowerCellAnimation() {
unsigned long holdTime = manualVentActive ? (millis() - manualVentStartTime) : manualVentDuration;
float ventIntensity = (float)holdTime / (float)MANUAL_VENT_THRESHOLD;
if (ventIntensity > 1.0) ventIntensity = 1.0;
powerCell.clear();
if (manualVentActive) {
// Active manual venting - show stress indicators
for (int i = 0; i < POWER_CELL_LEDS; i++) {
uint8_t baseBrightness = 100 - (uint8_t)(ventIntensity * 60);
// Progressive dimming from top down as vent gets more intense
float ledStress = (float)(POWER_CELL_LEDS - 1 - i) / (float)(POWER_CELL_LEDS - 1);
if (ledStress < ventIntensity) {
baseBrightness /= 2; // Dim significantly
// Add orange warning color
uint8_t warningLevel = (uint8_t)(ventIntensity * 80);
powerCell.setPixelColor(i, powerCell.Color(warningLevel, warningLevel/3, baseBrightness));
} else {
// Normal blue but dimmed
powerCell.setPixelColor(i, powerCell.Color(0, 0, baseBrightness));
}
// Random flickering on stressed LEDs
if (ventIntensity > 0.3 && random(0, 100) < 20) {
powerCell.setPixelColor(i, powerCell.Color(0, 0, 0)); // Random dropout
}
}
} else if (manualVentRecovery) {
// Recovery from manual vent
unsigned long recoveryTime = millis() - manualVentRecoveryStart;
float recoveryDuration = manualVentDuration * 0.5;
float recoveryProgress = (float)recoveryTime / recoveryDuration;
if (recoveryProgress > 1.0) recoveryProgress = 1.0;
// Gradual restoration from bottom up
int recoveredLEDs = (int)(recoveryProgress * POWER_CELL_LEDS);
for (int i = 0; i < POWER_CELL_LEDS; i++) {
if (i < recoveredLEDs) {
// Recovered LEDs - normal blue
uint8_t brightness = (uint8_t)(recoveryProgress * 120);
if (random(0, 100) < 10) brightness /= 2; // Occasional flicker
powerCell.setPixelColor(i, powerCell.Color(0, 0, brightness));
} else {
// Still recovering LEDs - dim and flickering
uint8_t dimBrightness = random(0, 40);
powerCell.setPixelColor(i, powerCell.Color(dimBrightness/2, 0, dimBrightness));
}
}
}
powerCell.show();
}
void spinCyclotron() {
cyclotron.clear();
// Enhanced cyclotron effects based on state
switch(currentState) {
case STATE_BOOT: {
// During boot, intensity increases with fill progress
unsigned long elapsed = millis() - stateStart;
float progress = (float)elapsed / (float)BOOT_SEQUENCE_DURATION;
if (progress > 1.0) progress = 1.0;
uint8_t intensity = (uint8_t)(progress * 255);
// 4 spinning segments with increasing brightness
for (int i = 0; i < 4; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 4)) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(center, cyclotron.Color(intensity, 0, 0));
// Trailing effects that get longer as we boot up
int trailLength = (int)(progress * 3) + 1;
for (int t = 1; t <= trailLength; t++) {
int trail = (center - t + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
uint8_t trailBrightness = intensity / (t + 1);
cyclotron.setPixelColor(trail, cyclotron.Color(trailBrightness, 0, 0));
}
}
break;
}
case STATE_IDLE:
if (justVented) {
// Post-vent recovery - cyclotron struggling to get back to normal
unsigned long elapsed = millis() - spinupStartTime;
float spinupDuration = BOOT_SEQUENCE_DURATION * 0.7;
float progress = (float)elapsed / spinupDuration;
if (progress > 1.0) progress = 1.0;
// Gradually restore normal operation
uint8_t intensity = (uint8_t)(progress * 255);
int activeSegments = 1 + (int)(progress * 3); // 1 to 4 segments
for (int i = 0; i < activeSegments; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 4)) % CYCLOTRON_LEDS;
uint8_t segmentBrightness = intensity;
// Early recovery shows inconsistent brightness
if (progress < 0.5 && random(0, 100) < 20) {
segmentBrightness = intensity / 2;
}
cyclotron.setPixelColor(center, cyclotron.Color(segmentBrightness, 0, 0));
// Trails get longer as we recover
int trailLength = (int)(progress * 2) + 1;
for (int t = 1; t <= trailLength; t++) {
int trail = (center - t + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
uint8_t trailBrightness = segmentBrightness / (t + 1);
cyclotron.setPixelColor(trail, cyclotron.Color(trailBrightness, 0, 0));
}
}
} else {
// Standard 4-segment rotation
for (int i = 0; i < 4; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 4)) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(center, cyclotron.Color(255, 0, 0));
// Short trails
int trail1 = (center - 1 + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
int trail2 = (center - 2 + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(trail1, cyclotron.Color(128, 0, 0));
cyclotron.setPixelColor(trail2, cyclotron.Color(64, 0, 0));
}
}
break;
case STATE_READY_BURST:
// Faster with blue tint
for (int i = 0; i < 4; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 4)) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(center, cyclotron.Color(255, 50, 100)); // Red with blue tint
int trail1 = (center - 1 + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
int trail2 = (center - 2 + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(trail1, cyclotron.Color(150, 25, 50));
cyclotron.setPixelColor(trail2, cyclotron.Color(75, 12, 25));
}
break;
case STATE_READY_FULL:
// Maximum intensity with longer trails
for (int i = 0; i < 4; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 4)) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(center, cyclotron.Color(255, 100, 0)); // Bright orange
// Longer trails for firing effect
for (int t = 1; t <= 4; t++) {
int trail = (center - t + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
uint8_t brightness = 255 / (t + 1);
cyclotron.setPixelColor(trail, cyclotron.Color(brightness, brightness/2, 0));
}
}
break;
case STATE_VENT: {
// Realistic venting effects - system in distress
unsigned long elapsed = millis() - stateStart;
float ventProgress = (float)elapsed / (float)VENT_DURATION;
if (ventProgress < 0.3) {
// Early vent - still some power, but struggling
for (int i = 0; i < 4; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 4)) % CYCLOTRON_LEDS;
// Flickering, inconsistent brightness
uint8_t intensity = random(50, 150);
if (random(0, 100) < 30) intensity = random(0, 50); // Random dim outs
cyclotron.setPixelColor(center, cyclotron.Color(intensity, intensity/4, 0)); // Orange distress
// Sporadic trails
if (random(0, 100) < 60) {
int trail1 = (center - 1 + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(trail1, cyclotron.Color(intensity/2, 0, 0));
}
}
} else if (ventProgress < 0.8) {
// Critical phase - barely functioning
// Only light 1-2 segments, very dim, lots of dead zones
int activeSegments = random(1, 3); // 1 or 2 segments max
for (int i = 0; i < activeSegments; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 4)) % CYCLOTRON_LEDS;
// Very dim, red only
uint8_t intensity = random(20, 60);
if (random(0, 100) < 40) intensity = 0; // Frequent dropouts
cyclotron.setPixelColor(center, cyclotron.Color(intensity, 0, 0));
}
// Random single pixel failures across the ring
if (random(0, 100) < 20) {
int randomPixel = random(0, CYCLOTRON_LEDS);
cyclotron.setPixelColor(randomPixel, cyclotron.Color(random(0, 30), 0, 0));
}
} else {
// Recovery phase - system trying to stabilize
for (int i = 0; i < 2; i++) { // Only 2 segments
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / 2)) % CYCLOTRON_LEDS;
uint8_t intensity = random(30, 80);
cyclotron.setPixelColor(center, cyclotron.Color(intensity, 0, 0));
// Short trail
int trail1 = (center - 1 + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
cyclotron.setPixelColor(trail1, cyclotron.Color(intensity/3, 0, 0));
}
}
break;
}
}
// Handle manual vent visual effects
if (manualVentActive || manualVentRecovery) {
unsigned long holdTime = manualVentActive ? (millis() - manualVentStartTime) : manualVentDuration;
float ventIntensity = (float)holdTime / (float)MANUAL_VENT_THRESHOLD;
if (ventIntensity > 1.0) ventIntensity = 1.0;
cyclotron.clear();
// Progressive degradation based on vent intensity
int activeSegments = 4 - (int)(ventIntensity * 2); // 4 down to 2 segments
if (activeSegments < 1) activeSegments = 1;
for (int i = 0; i < activeSegments; i++) {
int center = (cyclotronStep + (i * CYCLOTRON_LEDS / activeSegments)) % CYCLOTRON_LEDS;
// Reduce brightness and add orange tint during manual vent
uint8_t baseBrightness = (uint8_t)(255 * (1.0 - ventIntensity * 0.7));
uint8_t orangeTint = (uint8_t)(ventIntensity * 100);
// Flickering during intense manual vent
if (ventIntensity > 0.5 && random(0, 100) < 30) {
baseBrightness /= 2;
}
cyclotron.setPixelColor(center, cyclotron.Color(baseBrightness, orangeTint, 0));
// Shorter trails during venting
int trailLength = 2 - (int)(ventIntensity);
for (int t = 1; t <= trailLength; t++) {
int trail = (center - t + CYCLOTRON_LEDS) % CYCLOTRON_LEDS;
uint8_t trailBrightness = baseBrightness / (t + 1);
cyclotron.setPixelColor(trail, cyclotron.Color(trailBrightness, orangeTint/2, 0));
}
}
}
cyclotron.show();
cyclotronStep = (cyclotronStep + 1) % CYCLOTRON_LEDS;
}
void updateSound() {
if (!soundEnabled || soundState == SOUND_OFF) return;
unsigned long elapsed = millis() - soundStart;
switch (soundState) {
case SOUND_BOOT:
updateBootSound(elapsed);
break;
case SOUND_FIRE:
updateFireSound(elapsed);
break;
case SOUND_VENT:
updateVentSound(elapsed);
break;
case SOUND_CLICK:
updateClickSound(elapsed);
break;
}
}
void updateBootSound(unsigned long elapsed) {
static unsigned long lastTone = 0;
uint16_t totalDuration = 1500; // Total boot sound duration
if (elapsed > totalDuration) {
stopSound();
return;
}
if (elapsed - lastTone > 100) { // Change tone every 100ms
uint16_t freq = map(elapsed, 0, totalDuration, 200, 800);
tone(BUZZER_PIN, freq);
lastTone = elapsed;
}
}
void updateFireSound(unsigned long elapsed) {
static unsigned long lastTone = 0;
uint16_t totalDuration = FIRE_DURATION;
if (elapsed > totalDuration) {
stopSound();
return;
}
if (elapsed - lastTone > 50) { // Rapid firing sound
uint16_t freq = random(800, 1200);
tone(BUZZER_PIN, freq);
lastTone = elapsed;
}
}
void updateVentSound(unsigned long elapsed) {
static unsigned long lastTone = 0;
uint16_t totalDuration = VENT_DURATION;
if (elapsed > totalDuration) {
stopSound();
return;
}
float ventProgress = (float)elapsed / (float)totalDuration;
if (ventProgress < 0.3) {
// Critical overload phase - rapid, distressed beeping
if (elapsed - lastTone > 80) {
uint16_t freq = random(200, 400); // Low, distressed tones
tone(BUZZER_PIN, freq);
lastTone = elapsed;
}
} else if (ventProgress < 0.8) {
// Deep venting phase - very slow, deep tones
if (elapsed - lastTone > 300) {
uint16_t freq = map(elapsed, VENT_DURATION * 0.3, VENT_DURATION * 0.8, 150, 100);
tone(BUZZER_PIN, freq);
lastTone = elapsed;
}
} else {
// Recovery preparation - stabilizing tones
if (elapsed - lastTone > 200) {
uint16_t freq = map(elapsed, VENT_DURATION * 0.8, VENT_DURATION, 100, 200);
tone(BUZZER_PIN, freq);
lastTone = elapsed;
}
}
}
void updateClickSound(unsigned long elapsed) {
// Simple click sound - short mechanical click
uint16_t clickDuration = 150; // Very short click
if (elapsed > clickDuration) {
stopSound();
return;
}
// Two-tone click sound like a mechanical switch
if (elapsed < 50) {
tone(BUZZER_PIN, 800); // High click
} else if (elapsed < 100) {
tone(BUZZER_PIN, 400); // Low click
} else {
noTone(BUZZER_PIN); // Silent tail
}
}
void checkSerialCommands() {
if (Serial.available()) {
String command = Serial.readString();
command.trim();
if (command.startsWith("speed")) {
int newSpeed = command.substring(5).toInt();
if (newSpeed > 500 && newSpeed < 10000) {
// Update the boot sequence duration
Serial.print("Boot speed changed to: ");
Serial.print(newSpeed);
Serial.println("ms");
// Note: You'd need to make BOOT_SEQUENCE_DURATION a variable instead of #define
// For now, this shows the concept
} else {
Serial.println("Speed must be between 500-10000ms");
}
} else if (command == "status") {
Serial.print("Current state: ");
Serial.println(currentState);
Serial.print("Boot duration: ");
Serial.println(BOOT_SEQUENCE_DURATION);
Serial.print("Manual vent active: ");
Serial.println(manualVentActive ? "YES" : "NO");
Serial.print("Manual vent recovery: ");
Serial.println(manualVentRecovery ? "YES" : "NO");
if (justVented) Serial.println("Post-vent spinup in progress");
} else if (command == "pins") {
// Pin diagnostic command
Serial.println("=== PIN DIAGNOSTICS ===");
Serial.print("PIN 2 (Fire): ");
Serial.println(digitalRead(FIRE_BUTTON) ? "HIGH" : "LOW");
Serial.print("PIN 3 (Vent): ");
Serial.println(digitalRead(MANUAL_VENT_BTN) ? "HIGH" : "LOW");
Serial.print("PIN 5 (Master): ");
Serial.println(digitalRead(TOGGLE_MASTER) ? "HIGH" : "LOW");
Serial.print("PIN 6 (Ready): ");
Serial.println(digitalRead(TOGGLE_READY) ? "HIGH" : "LOW");
Serial.println("=== PROCESSED VALUES ===");
Serial.print("Master ON: ");
Serial.println(lastMasterToggle ? "YES" : "NO");
Serial.print("Ready ON: ");
Serial.println(lastReadyToggle ? "YES" : "NO");
} else if (command == "help") {
Serial.println("=== COMMANDS ===");
Serial.println("status - Show system status");
Serial.println("pins - Show pin states");
Serial.println("speed#### - Set boot speed (e.g. speed3000)");
Serial.println("ignore - Ignore toggle bounce (use if stuck)");
Serial.println("normal - Resume normal toggle operation");
Serial.println("help - Show this help");
} else if (command == "ignore") {
ignoreToggleBounce = true;
Serial.println("Toggle bounce protection DISABLED");
Serial.println("System will respond immediately to toggle changes");
Serial.println("Use 'normal' to re-enable protection");
} else if (command == "normal") {
ignoreToggleBounce = false;
Serial.println("Toggle bounce protection ENABLED");
Serial.println("Normal debouncing restored");
}
}
}