#include #include #include #include #include // ========================================== // ⚙️ USER CONFIGURATION (MODULAR) // ========================================== #define DEVICE_NAME "BlazeC3_Pod_01" // --- Feature Toggles (Comment to disable) --- // #define FEATURE_PIEZO #define FEATURE_BUTTON // #define FEATURE_ACCEL // --- Pin Assignments (ESP32-C3 Super Mini) --- #define LED_PIN 3 #ifdef FEATURE_PIEZO #define PIEZO_PIN 4 #define PIEZO_THRESHOLD 1500 #endif #ifdef FEATURE_BUTTON #define BUTTON_PIN 5 #define BUTTON_THRESHOLD 100 // Only used if analog read #endif #ifdef FEATURE_ACCEL #define ACCEL_X_PIN 6 #define ACCEL_Y_PIN 7 #define ACCEL_Z_PIN 0 #define ACCEL_THRESHOLD 150 #endif #define NUM_PIXELS 12 #define BRIGHTNESS 50 #define BREATHE_MAX 40 // ========================================== // --- BLE UUIDs --- #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" // --- Pod States --- enum PodState { STATE_BOOT, STATE_WAITING, STATE_CONNECTED, STATE_ACTIVE, STATE_SUCCESS }; // --- Color Structure --- struct LEDColor { uint8_t r, g, b; }; // --- Default Colors --- LEDColor colorWait = {0, 0, 255}; // Blue (breathing) LEDColor colorConn = {0, 255, 255}; // Cyan LEDColor colorActive = {255, 0, 0}; // Red LEDColor colorSuccess = {0, 255, 0}; // Green // --- Globals --- Adafruit_NeoPixel strip(NUM_PIXELS, LED_PIN, NEO_GRB + NEO_KHZ800); BLEServer *pServer = NULL; BLECharacteristic *pCharacteristic = NULL; bool deviceConnected = false; bool oldDeviceConnected = false; PodState currentState = STATE_BOOT; unsigned long stateStartTime = 0; unsigned long lastAnimTime = 0; int animStep = 0; unsigned long startTime = 0; unsigned long reactionTime = 0; bool gameActive = false; bool isTapped = false; int baseX = 0, baseY = 0, baseZ = 0; bool accelCalibrated = false; // --- Helper: Create Color --- uint32_t makeColor(LEDColor c) { return strip.Color(c.r, c.g, c.b); } // --- Helper: Set All LEDs --- void fillStrip(uint32_t color) { for (int i = 0; i < NUM_PIXELS; i++) { strip.setPixelColor(i, color); } strip.show(); } // --- Helper: Parse Color from String "R,G,B" --- LEDColor parseColor(String str) { LEDColor c = {0, 0, 0}; int comma1 = str.indexOf(','); int comma2 = str.lastIndexOf(','); if (comma1 > 0 && comma2 > comma1) { c.r = str.substring(0, comma1).toInt(); c.g = str.substring(comma1 + 1, comma2).toInt(); c.b = str.substring(comma2 + 1).toInt(); } return c; } // --- Helper: Format Color to String --- String formatColor(LEDColor c) { return String(c.r) + "," + String(c.g) + "," + String(c.b); } // --- BLE Callbacks --- class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; currentState = STATE_CONNECTED; stateStartTime = millis(); Serial.println("Device Connected"); } void onDisconnect(BLEServer* pServer) { deviceConnected = false; currentState = STATE_WAITING; stateStartTime = millis(); Serial.println("Device Disconnected"); } }; class MyCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { String value = pCharacteristic->getValue(); value.trim(); Serial.print("Received: ["); Serial.print(value); Serial.println("]"); // --- Color Commands --- if (value.startsWith("COLOR_WAIT:")) { colorWait = parseColor(value.substring(11)); pCharacteristic->setValue("OK:COLOR_WAIT:" + formatColor(colorWait)); pCharacteristic->notify(); } else if (value.startsWith("COLOR_CONN:")) { colorConn = parseColor(value.substring(11)); pCharacteristic->setValue("OK:COLOR_CONN:" + formatColor(colorConn)); pCharacteristic->notify(); } else if (value.startsWith("COLOR_ACTIVE:")) { colorActive = parseColor(value.substring(13)); pCharacteristic->setValue("OK:COLOR_ACTIVE:" + formatColor(colorActive)); pCharacteristic->notify(); } else if (value.startsWith("COLOR_SUCCESS:")) { colorSuccess = parseColor(value.substring(14)); pCharacteristic->setValue("OK:COLOR_SUCCESS:" + formatColor(colorSuccess)); pCharacteristic->notify(); } else if (value == "GET_COLORS") { String response = "COLORS|WAIT:" + formatColor(colorWait) + "|CONN:" + formatColor(colorConn) + "|ACTIVE:" + formatColor(colorActive) + "|SUCCESS:" + formatColor(colorSuccess); pCharacteristic->setValue(response); pCharacteristic->notify(); } else if (value == "RESET_COLORS") { colorWait = {0, 0, 255}; colorConn = {0, 255, 255}; colorActive = {255, 0, 0}; colorSuccess = {0, 255, 0}; pCharacteristic->setValue("OK:COLORS_RESET"); pCharacteristic->notify(); } // --- Game Commands --- else if (value == "START") { currentState = STATE_ACTIVE; fillStrip(makeColor(colorActive)); startTime = micros(); // Microsecond precision gameActive = true; isTapped = false; accelCalibrated = false; pCharacteristic->setValue("OK:STARTED"); pCharacteristic->notify(); Serial.println("Game Started"); } else if (value == "RESET") { currentState = STATE_CONNECTED; fillStrip(makeColor(colorConn)); gameActive = false; pCharacteristic->setValue("OK:RESET"); pCharacteristic->notify(); Serial.println("Game Reset"); } else { pCharacteristic->setValue("ERROR:UNKNOWN_COMMAND"); pCharacteristic->notify(); } } }; // --- Calibrate Accelerometer --- void calibrateAccel() { #ifdef FEATURE_ACCEL if (!accelCalibrated) { baseX = analogRead(ACCEL_X_PIN); baseY = analogRead(ACCEL_Y_PIN); baseZ = analogRead(ACCEL_Z_PIN); accelCalibrated = true; Serial.print("Accel Calibrated: "); Serial.print(baseX); Serial.print(", "); Serial.print(baseY); Serial.print(", "); Serial.println(baseZ); } #endif } // --- Check All Enabled Triggers --- void checkTriggers() { if (!gameActive || !deviceConnected || isTapped) return; bool triggered = false; // 1. Piezo Check (Analog) #ifdef FEATURE_PIEZO int piezoValue = analogRead(PIEZO_PIN); if (piezoValue > PIEZO_THRESHOLD) { triggered = true; Serial.println("Trigger: Piezo"); } #endif // 2. Button Check (Digital - More Reliable) #ifdef FEATURE_BUTTON #if !defined(FEATURE_PIEZO) || (BUTTON_PIN != PIEZO_PIN) // Use digital read for button (more reliable than analog) if (digitalRead(BUTTON_PIN) == LOW) { triggered = true; Serial.println("Trigger: Button"); } #endif #endif // 3. Accelerometer Check (Analog) #ifdef FEATURE_ACCEL if (accelCalibrated && !triggered) { int curX = analogRead(ACCEL_X_PIN); int curY = analogRead(ACCEL_Y_PIN); int curZ = analogRead(ACCEL_Z_PIN); int diffX = abs(curX - baseX); int diffY = abs(curY - baseY); int diffZ = abs(curZ - baseZ); if (diffX > ACCEL_THRESHOLD || diffY > ACCEL_THRESHOLD || diffZ > ACCEL_THRESHOLD) { triggered = true; Serial.println("Trigger: Accelerometer"); } } #endif // Process Trigger if (triggered) { isTapped = true; unsigned long endTime = micros(); // Microsecond precision reactionTime = (endTime - startTime) / 1000; // Convert to milliseconds // Send result with millisecond accuracy String result = "RESULT:" + String(reactionTime) + "ms"; pCharacteristic->setValue(result); pCharacteristic->notify(); currentState = STATE_SUCCESS; stateStartTime = millis(); fillStrip(makeColor(colorSuccess)); gameActive = false; Serial.print("Reaction Time: "); Serial.print(reactionTime); Serial.println("ms"); } } // --- Reset Trigger After Settle --- void resetTrigger() { if (isTapped) { bool settled = false; #ifdef FEATURE_PIEZO int piezoValue = analogRead(PIEZO_PIN); if (piezoValue < (PIEZO_THRESHOLD / 2)) settled = true; #elif defined(FEATURE_BUTTON) if (digitalRead(BUTTON_PIN) == HIGH) settled = true; // Button released #else if (millis() - stateStartTime > 2000) settled = true; #endif if (settled) { isTapped = false; accelCalibrated = false; } } } // --- Smart Animation Engine --- void handleAnimations() { unsigned long now = millis(); if (currentState == STATE_BOOT) { if (now - stateStartTime < 2000) { if (now - lastAnimTime > 50) { for (int i = 0; i < NUM_PIXELS; i++) { uint32_t color = strip.ColorHSV(i * 255 / NUM_PIXELS + animStep * 10, 255, BRIGHTNESS); strip.setPixelColor(i, color); } strip.show(); animStep++; lastAnimTime = now; } } else { currentState = STATE_WAITING; stateStartTime = now; fillStrip(0); } } // SMOOTH BREATHING - Gamma corrected, 10ms updates else if (currentState == STATE_WAITING) { if (now - lastAnimTime > 10) { float breath = sin(now / 800.0); // Slower, natural cycle float normalized = (breath + 1.0) / 2.0; // 0 to 1 float gammaCorrected = pow(normalized, 2.2); // Gamma correction int brightness = (int)(gammaCorrected * BREATHE_MAX); uint32_t color = strip.Color(colorWait.r * brightness / BREATHE_MAX, colorWait.g * brightness / BREATHE_MAX, colorWait.b * brightness / BREATHE_MAX); fillStrip(color); lastAnimTime = now; } } else if (currentState == STATE_CONNECTED) { if (now - lastAnimTime > 500) { fillStrip(makeColor(colorConn)); lastAnimTime = now; } } else if (currentState == STATE_ACTIVE) { if (now - lastAnimTime > 500) { fillStrip(makeColor(colorActive)); lastAnimTime = now; } } else if (currentState == STATE_SUCCESS) { if (now - stateStartTime > 2000) { currentState = STATE_CONNECTED; fillStrip(makeColor(colorConn)); } } } void setup() { Serial.begin(115200); while (!Serial); Serial.println("================================"); Serial.println(" BlazePod C3 - Production Ready"); Serial.println("================================"); #ifdef FEATURE_PIEZO Serial.println("Piezo: ON"); #else Serial.println("Piezo: OFF"); #endif #ifdef FEATURE_BUTTON Serial.println("Button: ON"); #else Serial.println("Button: OFF"); #endif #ifdef FEATURE_ACCEL Serial.println("Accel: ON"); #else Serial.println("Accel: OFF"); #endif Serial.println("================================"); pinMode(LED_PIN, OUTPUT); #ifdef FEATURE_PIEZO pinMode(PIEZO_PIN, INPUT); #endif #ifdef FEATURE_BUTTON pinMode(BUTTON_PIN, INPUT_PULLUP); #endif #ifdef FEATURE_ACCEL pinMode(ACCEL_X_PIN, INPUT); pinMode(ACCEL_Y_PIN, INPUT); pinMode(ACCEL_Z_PIN, INPUT); #endif analogReadResolution(12); strip.begin(); strip.setBrightness(BRIGHTNESS); strip.show(); fillStrip(0); currentState = STATE_BOOT; stateStartTime = millis(); BLEDevice::init(DEVICE_NAME); pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); BLEService *pService = pServer->createService(SERVICE_UUID); pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY ); pCharacteristic->setCallbacks(new MyCallbacks()); pCharacteristic->addDescriptor(new BLE2902()); pService->start(); BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(true); BLEDevice::startAdvertising(); Serial.println("System Ready - Waiting for connection..."); } void loop() { if (!deviceConnected && oldDeviceConnected) { delay(500); pServer->startAdvertising(); oldDeviceConnected = deviceConnected; } if (deviceConnected && !oldDeviceConnected) { oldDeviceConnected = deviceConnected; } handleAnimations(); if (gameActive && !accelCalibrated) { calibrateAccel(); } checkTriggers(); resetTrigger(); delay(10); }