#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#define LED_PIN 36
#define NUM_PIXELS 7
#define BUTTON_PIN 4 // Try 4, 5, 18, 19, 21, 22, or 23
#define LIGHT_SENSOR_PIN 5 // LDR pin for brightness control
Adafruit_NeoPixel lantern(NUM_PIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_MPU6050 mpu;
float speedFactor = 1.0; // Speed multiplier for effects
// Light sensor variables for LDR brightness control
int lightLevel = 0; // Current light sensor reading (0-4095)
float brightness = 1.0; // Brightness multiplier (0.1 to 1.0)
// Button variables
bool lastButtonState = HIGH;
bool buttonPressed = false;
unsigned long buttonPressTime = 0;
unsigned long lastValidPress = 0;
const unsigned long pressTimeout = 300; // Ignore multiple presses within 300ms
bool singlePressMode = true; // Enable/disable single press detection
// Three-press activation
int pressCount = 0;
// Lamp state
bool lampLit = false;
int currentEffectIndex = 0;
// Timing for effects
unsigned long lastEffectUpdate = 0;
unsigned long effectUpdateInterval = 100;
// ===============================================================================
// EFFECT FUNCTIONS
// ===============================================================================
void updateFlickerEffect() {
for (int i = 0; i < NUM_PIXELS; i++) {
int flicker = random(100, 255);
lantern.setPixelColor(i, lantern.Color(flicker, random(30, 80), 0));
}
lantern.show();
}
void updateGlowEffect() {
static float brightness = 30;
static float direction = 3;
brightness += direction;
if (brightness >= 255 || brightness <= 30) direction *= -1;
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, lantern.Color(
(255 * brightness) / 255,
(140 * brightness) / 255,
0
));
}
lantern.show();
}
// ===============================================================================
// SIMPLE LIGHT SENSOR FUNCTION
// ===============================================================================
void checkLightAndUpdateBrightness() {
// Read light sensor (0 = dark, 4095 = bright)
lightLevel = analogRead(LIGHT_SENSOR_PIN);
// Map light to brightness: MORE light = DIMMER LEDs, LESS light = BRIGHTER LEDs
// Light 0-4095 maps to brightness 1.0-0.1
brightness = map(lightLevel, 0, 4095, 100, 10) / 100.0; // 1.0 (bright) to 0.1 (dim)
// Keep brightness within safe range
if (brightness > 1.0) brightness = 1.0;
if (brightness < 0.1) brightness = 0.1;
}
// Global flag for ignition completion
bool ignitionComplete = false;
uint32_t dimColor(uint8_t r, uint8_t g, uint8_t b) {
// Apply brightness to color
r = r * brightness;
g = g * brightness;
b = b * brightness;
return lantern.Color(r, g, b);
}
void updateCarbideIgnitionEffect() {
static int ignitionStage = 0;
static unsigned long stageTimer = 0;
static unsigned long lastUpdate = 0;
unsigned long currentTime = millis();
switch(ignitionStage) {
case 0: // Spark attempt
if (currentTime - lastUpdate >= 100) {
lantern.clear();
if (random(100) < 30) { // 30% chance of spark
lantern.setPixelColor(random(NUM_PIXELS), lantern.Color(255, 255, 255));
}
lantern.show();
if (currentTime - stageTimer >= 1000) {
ignitionStage = 1;
stageTimer = currentTime;
}
lastUpdate = currentTime;
}
break;
case 1: // Gas buildup
if (currentTime - lastUpdate >= 50) {
lantern.clear();
int gasPixels = random(1, 4);
for (int i = 0; i < gasPixels; i++) {
lantern.setPixelColor(random(NUM_PIXELS), lantern.Color(0, 50, 100));
}
lantern.show();
if (currentTime - stageTimer >= 800) {
ignitionStage = 2;
stageTimer = currentTime;
}
lastUpdate = currentTime;
}
break;
case 2: // IGNITION!
if (currentTime - lastUpdate >= 30) {
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, lantern.Color(255, 255, 200)); // Bright white-yellow
}
lantern.show();
if (currentTime - stageTimer >= 500) {
ignitionStage = 3;
stageTimer = currentTime;
}
lastUpdate = currentTime;
}
break;
case 3: // Settle to steady flame
ignitionStage = 0; // Reset for next time
ignitionComplete = true; // Signal that ignition is done
break;
}
}
void updateCarbideFlameEffect() {
// Bright, steady carbide flame with subtle flicker
for (int i = 0; i < NUM_PIXELS; i++) {
int brightness = random(200, 255);
lantern.setPixelColor(i, lantern.Color(brightness, brightness, brightness - 50));
}
lantern.show();
}
void updateCarbideFlutterEffect() {
// Wind affecting the carbide flame
static float windPhase = 0;
for (int i = 0; i < NUM_PIXELS; i++) {
float wind = sin(windPhase + i * 0.3) * 0.3 + 0.7; // 0.4 to 1.0
int brightLevel = wind * 255;
lantern.setPixelColor(i, dimColor(brightLevel, brightLevel, brightLevel - 30));
}
windPhase += 0.2;
if (windPhase >= 6.28) windPhase = 0;
lantern.show();
}
void updateMinerFlameEffect() {
// Stable mining lamp flame
static float breathe = 0;
breathe += 0.05;
float brightLevel = (sin(breathe) * 0.1 + 0.9) * 255; // 80% to 100% brightness
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(brightLevel, brightLevel * 0.9, brightLevel * 0.7));
}
lantern.show();
}
void updateGasLeakEffect() {
// Simulating gas escaping before ignition
static int leakPixel = 0;
static unsigned long lastLeak = 0;
unsigned long currentTime = millis();
if (currentTime - lastLeak >= 150) {
lantern.clear();
// Show gas "escaping" from one pixel
lantern.setPixelColor(leakPixel, dimColor(0, 30, 80));
if (random(100) < 20) { // 20% chance of stronger gas puff
lantern.setPixelColor((leakPixel + 1) % NUM_PIXELS, dimColor(0, 20, 60));
}
leakPixel = (leakPixel + 1) % NUM_PIXELS;
lantern.show();
lastLeak = currentTime;
}
}
void updateHeadlampBeamEffect() {
// Focused beam like a mining headlamp
static bool sweeping = false;
static int beamPosition = NUM_PIXELS / 2;
static int direction = 1;
static unsigned long lastMove = 0;
unsigned long currentTime = millis();
if (currentTime - lastMove >= 300) {
lantern.clear();
// Main beam
lantern.setPixelColor(beamPosition, dimColor(255, 255, 200));
// Side illumination
if (beamPosition > 0) {
lantern.setPixelColor(beamPosition - 1, dimColor(100, 100, 80));
}
if (beamPosition < NUM_PIXELS - 1) {
lantern.setPixelColor(beamPosition + 1, dimColor(100, 100, 80));
}
if (sweeping) {
beamPosition += direction;
if (beamPosition >= NUM_PIXELS - 1 || beamPosition <= 0) {
direction *= -1;
}
}
lantern.show();
lastMove = currentTime;
}
}
void updateOldFlameEffect() {
// Vintage carbide lamp flame with period-appropriate color
for (int i = 0; i < NUM_PIXELS; i++) {
int flicker = random(180, 255);
// Warmer, more yellow flame typical of carbide
lantern.setPixelColor(i, dimColor(flicker, flicker * 0.8, flicker * 0.3));
}
lantern.show();
}
void updateCaveExplorerEffect() {
// Dim, conserved light for cave exploration
static float dimmer = 0;
dimmer += 0.03;
float caveBright = (sin(dimmer) * 0.2 + 0.8) * 120; // Very dim, 60-100 brightness
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(caveBright, caveBright * 0.9, caveBright * 0.6));
}
lantern.show();
}
void updateLowFuelEffect() {
// Sputtering flame running out of carbide
static unsigned long lastSputter = 0;
static bool isLit = true;
unsigned long currentTime = millis();
if (currentTime - lastSputter >= random(100, 800)) {
if (isLit) {
// Weak flame
for (int i = 0; i < NUM_PIXELS; i++) {
int weak = random(50, 120);
lantern.setPixelColor(i, dimColor(weak, weak * 0.8, weak * 0.5));
}
} else {
// Sputter out briefly
lantern.clear();
}
lantern.show();
isLit = !isLit;
lastSputter = currentTime;
}
}
void updateCanaryWarningEffect() {
// Yellow warning for dangerous gases
static bool warning = false;
static unsigned long lastBlink = 0;
unsigned long currentTime = millis();
if (currentTime - lastBlink >= (warning ? 200 : 500)) {
if (warning) {
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(255, 255, 0)); // Bright yellow
}
} else {
lantern.clear();
}
lantern.show();
warning = !warning;
lastBlink = currentTime;
}
}
void updateShiftChangeEffect() {
// Signal for shift changes - three long pulses
static int pulseCount = 0;
static bool pulseOn = false;
static unsigned long lastPulse = 0;
unsigned long currentTime = millis();
if (currentTime - lastPulse >= (pulseOn ? 800 : 400)) {
if (pulseOn) {
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(255, 255, 255));
}
pulseCount++;
} else {
lantern.clear();
}
lantern.show();
pulseOn = !pulseOn;
lastPulse = currentTime;
if (pulseCount >= 3 && !pulseOn) {
pulseCount = 0;
lastPulse = currentTime + 2000; // Long pause between cycles
}
}
}
void updateUndergroundStormEffect() {
// Simulating storm affecting underground air currents
static unsigned long lastGust = 0;
unsigned long currentTime = millis();
if (currentTime - lastGust >= random(50, 300)) {
for (int i = 0; i < NUM_PIXELS; i++) {
int storm = random(80, 200);
lantern.setPixelColor(i, dimColor(storm, storm * 0.9, storm * 0.7));
}
lantern.show();
lastGust = currentTime;
}
}
void updateCaveInEmergencyEffect() {
// Emergency red flashing
static bool emergency = false;
static unsigned long lastFlash = 0;
unsigned long currentTime = millis();
if (currentTime - lastFlash >= 150) {
if (emergency) {
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(255, 0, 0)); // Bright red
}
} else {
lantern.clear();
}
lantern.show();
emergency = !emergency;
lastFlash = currentTime;
}
}
void updateGoldRushEffect() {
// Golden shimmering for gold prospecting
for (int i = 0; i < NUM_PIXELS; i++) {
int gold = random(150, 255);
lantern.setPixelColor(i, dimColor(gold, gold * 0.8, 0));
}
lantern.show();
}
void updateCoalMineEffect() {
// Dimmer, sooty flame for coal mining
static float soot = 0;
soot += 0.04;
float coalBright = (sin(soot) * 0.3 + 0.7) * 180;
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(coalBright, coalBright * 0.6, coalBright * 0.3));
}
lantern.show();
}
void updateDrippingWaterEffect() {
// Cave water dripping affecting flame
static unsigned long lastDrip = 0;
static int dripPixel = 0;
static bool dripping = false;
unsigned long currentTime = millis();
if (currentTime - lastDrip >= (dripping ? 100 : random(1000, 3000))) {
if (dripping) {
// Normal flame except for drip pixel
for (int i = 0; i < NUM_PIXELS; i++) {
if (i == dripPixel) {
lantern.setPixelColor(i, dimColor(50, 100, 150)); // Water blue
} else {
lantern.setPixelColor(i, dimColor(200, 180, 120));
}
}
dripping = false;
dripPixel = random(NUM_PIXELS);
} else {
// Normal carbide flame
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(220, 200, 140));
}
dripping = true;
}
lantern.show();
lastDrip = currentTime;
}
}
void updateTunnelSurveyEffect() {
// Methodical scanning beam for surveying
static int scanPosition = 0;
static unsigned long lastScan = 0;
unsigned long currentTime = millis();
if (currentTime - lastScan >= 400) {
lantern.clear();
// Scanning beam
lantern.setPixelColor(scanPosition, dimColor(255, 255, 255));
// Trail
if (scanPosition > 0) {
lantern.setPixelColor(scanPosition - 1, dimColor(100, 100, 100));
}
scanPosition = (scanPosition + 1) % NUM_PIXELS;
lantern.show();
lastScan = currentTime;
}
}
void updateMorseCodeEffect() {
// Emergency morse code SOS
static int morseIndex = 0;
static bool morseOn = false;
static unsigned long lastMorse = 0;
// SOS pattern: ... --- ... (short short short long long long short short short)
int sosPattern[] = {200, 200, 200, 200, 200, 500, 600, 600, 600, 500, 200, 200, 200, 1000};
int patternLength = 14;
unsigned long currentTime = millis();
if (currentTime - lastMorse >= sosPattern[morseIndex]) {
if (morseIndex % 2 == 0) { // Even indices are "on" times
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(255, 255, 255));
}
} else { // Odd indices are "off" times
lantern.clear();
}
lantern.show();
morseIndex = (morseIndex + 1) % patternLength;
lastMorse = currentTime;
}
}
void updateFlameAdjustmentEffect() {
// Simulating adjusting the carbide feed
static float adjustment = 0;
static bool increasing = true;
if (increasing) {
adjustment += 0.02;
if (adjustment >= 1.0) increasing = false;
} else {
adjustment -= 0.02;
if (adjustment <= 0.2) increasing = true;
}
int adjustBright = adjustment * 255;
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(adjustBright, adjustBright * 0.9, adjustBright * 0.7));
}
lantern.show();
}
void updateGhostLightEffect() {
// Mysterious underground phenomena
static float ghost = 0;
ghost += 0.08;
for (int i = 0; i < NUM_PIXELS; i++) {
float shimmer = sin(ghost + i * 0.5) * 0.5 + 0.5;
int ghostBright = shimmer * 150;
lantern.setPixelColor(i, dimColor(ghostBright * 0.8, ghostBright, ghostBright * 0.9));
}
lantern.show();
}
void updateMineshaftBreezeEffect() {
// Gentle air current through shaft
static float breeze = 0;
breeze += 0.05;
for (int i = 0; i < NUM_PIXELS; i++) {
float wave = sin(breeze + i * 0.8) * 0.3 + 0.7;
int breezeBright = wave * 200;
lantern.setPixelColor(i, dimColor(breezeBright, breezeBright * 0.85, breezeBright * 0.6));
}
lantern.show();
}
void updateProspectorCelebrationEffect() {
// Found gold! Celebration effect
static unsigned long lastSparkle = 0;
unsigned long currentTime = millis();
if (currentTime - lastSparkle >= 80) {
for (int i = 0; i < NUM_PIXELS; i++) {
if (random(100) < 40) { // 40% chance of sparkle
lantern.setPixelColor(i, dimColor(255, 255, 0)); // Gold
} else {
lantern.setPixelColor(i, dimColor(255, 200, 100)); // Warm base
}
}
lantern.show();
lastSparkle = currentTime;
}
}
void updateColorShiftEffect() {
static int hue = 0;
for (int i = 0; i < NUM_PIXELS; i++) {
uint32_t color = lantern.ColorHSV(hue, 255, 150);
uint8_t r = (color >> 16) & 0xFF;
uint8_t g = (color >> 8) & 0xFF;
uint8_t b = color & 0xFF;
lantern.setPixelColor(i, dimColor(r, g, b));
}
hue += 200;
if (hue >= 65535) hue = 0;
lantern.show();
}
void updateStrobeEffect() {
static bool isOn = false;
static unsigned long lastToggle = 0;
unsigned long currentTime = millis();
if (currentTime - lastToggle >= 100) {
if (isOn) {
for (int j = 0; j < NUM_PIXELS; j++) {
lantern.setPixelColor(j, dimColor(255, 255, 255));
}
} else {
lantern.clear();
}
lantern.show();
isOn = !isOn;
lastToggle = currentTime;
}
}
void updatePoliceStrobeEffect() {
static bool redPhase = true;
static unsigned long lastToggle = 0;
unsigned long currentTime = millis();
if (currentTime - lastToggle >= 200) {
if (redPhase) {
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(255, 0, 0));
}
} else {
for (int i = 0; i < NUM_PIXELS; i++) {
lantern.setPixelColor(i, dimColor(0, 0, 255));
}
}
lantern.show();
redPhase = !redPhase;
lastToggle = currentTime;
}
}
// ===============================================================================
// EFFECT WRAPPER FUNCTIONS
// ===============================================================================
void effectCarbideIgnition() {
effectUpdateInterval = 30;
updateCarbideIgnitionEffect();
}
void effectCarbideFlame() {
effectUpdateInterval = 80;
updateCarbideFlameEffect();
}
void effectCarbideFlutter() {
effectUpdateInterval = 50;
updateCarbideFlutterEffect();
}
void effectMinerFlame() {
effectUpdateInterval = 40;
updateMinerFlameEffect();
}
void effectGasLeak() {
effectUpdateInterval = 150;
updateGasLeakEffect();
}
void effectHeadlampBeam() {
effectUpdateInterval = 100;
updateHeadlampBeamEffect();
}
void effectOldFlame() {
effectUpdateInterval = 90;
updateOldFlameEffect();
}
void effectCaveExplorer() {
effectUpdateInterval = 30;
updateCaveExplorerEffect();
}
void effectLowFuel() {
effectUpdateInterval = 100;
updateLowFuelEffect();
}
void effectCanaryWarning() {
effectUpdateInterval = 50;
updateCanaryWarningEffect();
}
void effectShiftChange() {
effectUpdateInterval = 50;
updateShiftChangeEffect();
}
void effectUndergroundStorm() {
effectUpdateInterval = 60;
updateUndergroundStormEffect();
}
void effectCaveInEmergency() {
effectUpdateInterval = 150;
updateCaveInEmergencyEffect();
}
void effectGoldRush() {
effectUpdateInterval = 70;
updateGoldRushEffect();
}
void effectCoalMine() {
effectUpdateInterval = 40;
updateCoalMineEffect();
}
void effectDrippingWater() {
effectUpdateInterval = 100;
updateDrippingWaterEffect();
}
void effectTunnelSurvey() {
effectUpdateInterval = 400;
updateTunnelSurveyEffect();
}
void effectMorseCode() {
effectUpdateInterval = 50;
updateMorseCodeEffect();
}
void effectFlameAdjustment() {
effectUpdateInterval = 40;
updateFlameAdjustmentEffect();
}
void effectGhostLight() {
effectUpdateInterval = 30;
updateGhostLightEffect();
}
void effectMineshaftBreeze() {
effectUpdateInterval = 50;
updateMineshaftBreezeEffect();
}
void effectProspectorCelebration() {
effectUpdateInterval = 80;
updateProspectorCelebrationEffect();
}
void effectFlicker() {
effectUpdateInterval = 100;
updateFlickerEffect();
}
void effectGlow() {
effectUpdateInterval = 20;
updateGlowEffect();
}
void effectColorShift() {
effectUpdateInterval = 30;
updateColorShiftEffect();
}
void effectStrobe() {
effectUpdateInterval = 50;
updateStrobeEffect();
}
void effectPoliceStrobe() {
effectUpdateInterval = 50;
updatePoliceStrobeEffect();
}
// Array of effect functions
typedef void (*EffectFunction)();
EffectFunction effects[] = {
effectCarbideIgnition, // Special ignition sequence
effectCarbideFlame, // Bright steady carbide flame
effectCarbideFlutter, // Wind-affected flame
effectMinerFlame, // Stable mining lamp
effectGasLeak, // Pre-ignition gas effect
effectHeadlampBeam, // Focused mining headlamp
effectOldFlame, // Vintage carbide color
effectCaveExplorer, // Dim conservation mode
effectLowFuel, // Sputtering low carbide
effectCanaryWarning, // Gas danger warning
effectShiftChange, // Shift signal pulses
effectUndergroundStorm, // Storm air currents
effectCaveInEmergency, // Emergency red flash
effectGoldRush, // Gold prospecting shimmer
effectCoalMine, // Sooty coal mine flame
effectDrippingWater, // Cave water drips
effectTunnelSurvey, // Surveying scan beam
effectMorseCode, // Emergency SOS
effectFlameAdjustment, // Adjusting carbide feed
effectGhostLight, // Mysterious phenomena
effectMineshaftBreeze, // Gentle air current
effectProspectorCelebration, // Found gold celebration
effectFlicker, // Classic candle flicker
effectGlow, // Breathing glow
effectColorShift, // Rainbow transitions
effectStrobe, // Emergency strobe
effectPoliceStrobe // Red/blue emergency
};
const int numEffects = sizeof(effects) / sizeof(effects[0]);
// Effect names for debugging
const char* effectNames[] = {
"Carbide Ignition", // š„ The special ignition sequence!
"Carbide Flame", // Bright white steady flame
"Carbide Flutter", // Wind-blown flame
"Miner Flame", // Stable work light
"Gas Leak", // Pre-ignition buildup
"Headlamp Beam", // Focused mining beam
"Old Flame", // Vintage carbide color
"Cave Explorer", // Conservation dim mode
"Low Fuel", // Sputtering carbide
"Canary Warning", // Gas danger alert
"Shift Change", // Work shift signals
"Underground Storm", // Storm air effects
"Cave-In Emergency", // Emergency red alert
"Gold Rush", // Prospecting shimmer
"Coal Mine", // Sooty mine flame
"Dripping Water", // Cave water effect
"Tunnel Survey", // Scanning beam
"Morse Code SOS", // Emergency morse
"Flame Adjustment", // Carbide feed tuning
"Ghost Light", // Mysterious glow
"Mineshaft Breeze", // Gentle air current
"Prospector Celebration", // Found gold party!
"Flicker", // Classic flicker
"Glow", // Breathing effect
"Color Shift", // Rainbow
"Strobe", // White strobe
"Police Strobe" // Emergency colors
};
float mapFloat(float x, float in_min, float in_max, float out_min, float out_max) {
if (x < in_min) x = in_min;
if (x > in_max) x = in_max;
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
void updateSpeedFromMPU() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
// Calculate acceleration magnitude (m/s^2)
float accelMagnitude = sqrt(a.acceleration.x * a.acceleration.x +
a.acceleration.y * a.acceleration.y +
a.acceleration.z * a.acceleration.z);
// Map acceleration magnitude to speed factor
// 9.8 m/s^2 = stationary, higher means movement
// Map 9.8 - 20 m/s^2 to speed factor 1.0 (slow) to 0.3 (fast)
speedFactor = mapFloat(accelMagnitude * 100, 980, 2000, 100, 30) / 100.0;
// Clamp speedFactor between 0.3 and 1.0
if (speedFactor < 0.3) speedFactor = 0.3;
if (speedFactor > 1.0) speedFactor = 1.0;
Serial.printf("Accel: %.2f m/s² | Speed factor: %.2f\n", accelMagnitude, speedFactor);
}
// ===============================================================================
// SETUP AND MAIN LOOP
// ===============================================================================
void setup() {
Wire.begin(19, 18); // Wire.begin(SDA, SCL);
Serial.begin(115200);
// Initialize NeoPixels
lantern.begin();
lantern.clear();
lantern.show();
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
Serial.println("MPU6050 Found!");
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
// Setup button with pullup resistor
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Setup light sensor
pinMode(LIGHT_SENSOR_PIN, INPUT);
checkLightAndUpdateBrightness(); // Initial reading
Serial.println("=== Professional Carbide Lamp Controller ===");
Serial.printf("NeoPixels on pin %d\n", LED_PIN);
Serial.printf("Button on pin %d\n", BUTTON_PIN);
Serial.printf("Light sensor on pin %d\n", LIGHT_SENSOR_PIN);
Serial.println("š” LIGHT SENSOR: More light = dimmer LEDs, less light = brighter LEDs");
Serial.println("š ESP32 VARIANT PIN GUIDE:");
Serial.println(" ESP32-C3: ADC pins: 0, 1, 2, 3, 4 | GPIO pins: 0-10, 18, 19");
Serial.println(" ESP32-S3: ADC pins: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 | GPIO pins: 0-21, 35-48");
Serial.println(" š” Adjust pin definitions if your wiring is different!");
Serial.printf("Single Press Mode: %s\n", singlePressMode ? "ENABLED" : "DISABLED");
Serial.printf("Total effects: %d\n", numEffects);
Serial.println("āļø CARBIDE LAMP ACTIVATION FLOW:");
Serial.println(" Press 1: Turn on power indicator");
Serial.println(" Press 2: WiFi stage (placeholder)");
Serial.println(" Press 3: Start CARBIDE IGNITION sequence!");
Serial.println("š” When ON: Single press cycles through all carbide effects");
Serial.println("š„ Featured effects: Ignition, Low Fuel, Canary Warning, Gold Rush, SOS, Cave-In Emergency");
Serial.println("āļø Mining modes: Shift Change, Tunnel Survey, Underground Storm, Dripping Water");
Serial.println("⨠Special modes: Ghost Light, Prospector Celebration, Flame Adjustment");
Serial.println("Serial commands:");
Serial.println(" 'toggle' - change press mode");
Serial.println(" 'status' - show complete state");
Serial.println(" 'reset' - reset system");
Serial.println(" 'light' - show light sensor reading");
// Show power indicator (dim light on first pixel)
showPowerIndicator();
Serial.printf("Initial light level: %d | Brightness: %.1f\n", lightLevel, brightness);
}
void loop() {
unsigned long currentTime = millis();
updateSpeedFromMPU();
// Check light sensor and update brightness
checkLightAndUpdateBrightness();
// Adjust effect update interval by speed factor
unsigned long adjustedInterval = (unsigned long)(effectUpdateInterval * speedFactor);
// Existing ignition, serial commands, button handling...
handleButton();
// Run current effect if lamp is on, using adjusted interval
if (lampLit) {
if (currentTime - lastEffectUpdate >= adjustedInterval) {
effects[currentEffectIndex]();
lastEffectUpdate = currentTime;
}
}
// Optional status debug...
// Check for ignition completion
if (ignitionComplete && currentEffectIndex == 0) {
currentEffectIndex = 1; // Switch to Carbide Flame
ignitionComplete = false; // Reset flag
Serial.println("š„ Ignition sequence complete - Now running Carbide Flame");
}
// Check for Serial commands
if (Serial.available()) {
String command = Serial.readString();
command.trim();
if (command == "toggle") {
singlePressMode = !singlePressMode;
Serial.printf("Single Press Mode: %s\n", singlePressMode ? "ENABLED" : "DISABLED");
} else if (command == "status") {
Serial.printf("Status: %s | Effect: %s | Single Press: %s | Press Count: %d/3\n",
lampLit ? "ON" : "OFF",
lampLit ? effectNames[currentEffectIndex] : "Power Indicator",
singlePressMode ? "ENABLED" : "DISABLED",
pressCount);
} else if (command == "reset") {
pressCount = 0;
lampLit = false;
Serial.println("System reset - back to start");
showPowerIndicator();
} else if (command == "light") {
Serial.printf("Light sensor: %d (0=dark, 4095=bright) | Brightness: %.1f (0.1=dim, 1.0=bright)\n", lightLevel, brightness);
}
}
// Handle button presses
handleButton();
// Run current effect if lamp is on
if (lampLit) {
runCurrentEffect(currentTime);
}
// Optional: Status debug every 15 seconds
static unsigned long lastStatus = 0;
if (currentTime - lastStatus > 15000) {
String stage = "Power Off";
if (!lampLit) {
if (pressCount == 1) stage = "Power Indicator";
else if (pressCount == 2) stage = "WiFi Stage";
} else {
stage = "Lamp On";
}
Serial.printf("Status: %s | Effect: %s | Press: %d/3 | Light: %d | Brightness: %.1f\n",
stage.c_str(),
lampLit ? effectNames[currentEffectIndex] : "N/A",
pressCount, lightLevel, brightness);
lastStatus = currentTime;
}
}
// ===============================================================================
// BUTTON HANDLING
// ===============================================================================
void handleButton() {
int reading = digitalRead(BUTTON_PIN);
unsigned long currentTime = millis();
// Detect button press (HIGH to LOW transition)
if (reading == LOW && lastButtonState == HIGH) {
if (singlePressMode) {
// Single press mode: ignore rapid presses
if (currentTime - lastValidPress > pressTimeout) {
Serial.println("š Single press detected!");
lastValidPress = currentTime;
handleButtonAction();
} else {
Serial.println("Button press ignored (single press mode - too soon)");
}
} else {
// Multi-press mode: allow rapid presses
Serial.println("š Button press detected!");
lastValidPress = currentTime;
handleButtonAction();
}
}
lastButtonState = reading;
}
void handleButtonAction() {
if (!lampLit) {
// LAMP IS OFF - Three stage activation
pressCount++;
if (pressCount == 1) {
// First press - Turn on power indicator
Serial.println("š” Press 1: Power indicator ON");
showPowerIndicator();
}
else if (pressCount == 2) {
// Second press - Placeholder for WiFi connection
Serial.println("š¶ Press 2: (WiFi connection placeholder - coming soon!)");
showWiFiPlaceholder();
}
else if (pressCount >= 3) {
// Third press - Turn on lamp with CARBIDE IGNITION effect
lampLit = true;
currentEffectIndex = 0; // Start with Carbide Ignition effect
pressCount = 0; // Reset counter
Serial.println("š„ Press 3: CARBIDE LAMP IGNITING!");
Serial.printf("š” Starting: %s\n", effectNames[currentEffectIndex]);
}
}
else {
// LAMP IS ON - Single press cycles effects
currentEffectIndex++;
if (currentEffectIndex >= numEffects) {
// Turn off lamp after cycling through all effects
lampLit = false;
currentEffectIndex = 0;
pressCount = 0; // Reset activation counter
lantern.clear();
lantern.show();
showPowerIndicator();
Serial.println("š” Lamp OFF");
} else {
Serial.printf("šØ Cycling to effect: %s\n", effectNames[currentEffectIndex]);
}
}
}
// ===============================================================================
// EFFECT MANAGEMENT
// ===============================================================================
void runCurrentEffect(unsigned long currentTime) {
if (currentEffectIndex >= 0 && currentEffectIndex < numEffects) {
if (currentTime - lastEffectUpdate >= effectUpdateInterval) {
effects[currentEffectIndex]();
lastEffectUpdate = currentTime;
}
}
}
void showPowerIndicator() {
lantern.clear();
// Show dim orange light on first pixel to indicate power
lantern.setPixelColor(0, dimColor(20, 10, 0));
lantern.show();
}
void showWiFiPlaceholder() {
// Show blue indicator for WiFi stage (placeholder)
lantern.clear();
lantern.setPixelColor(0, dimColor(20, 10, 0)); // Keep power indicator
lantern.setPixelColor(1, dimColor(0, 10, 30)); // Add blue for WiFi stage
lantern.show();
}
void blinkActivationFeedback(int pressNumber) {
// Visual feedback for activation sequence
lantern.clear();
// Light up pixels based on press number
for (int i = 0; i < pressNumber; i++) {
lantern.setPixelColor(i, dimColor(0, 100, 0)); // Green progress indicator
}
lantern.show();
delay(200); // Brief visual feedback
// Return to power indicator if not fully activated
if (pressNumber < 3) {
showPowerIndicator();
}
}