diff --git a/code/code.ino b/code/code.ino new file mode 100644 index 0000000..9a5352a --- /dev/null +++ b/code/code.ino @@ -0,0 +1,469 @@ +#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); +} \ No newline at end of file