// ============================================================ // INTERMISSION OBJECT — LED / MOTOR / HAPTIC ONLY // Temporary diagnostic sketch — audio disabled. // AUTO_START bypasses Hall sensor: experience begins 5 s after boot. // ============================================================ #include #include #include "DFRobot_TM6605.h" #include "wifi_ota.h" #define AUTO_START // remove to restore Hall-sensor trigger // ── Pins ───────────────────────────────────────────────────── #define PIN_I2C_SDA 5 #define PIN_I2C_SCL 6 #define PIN_LED_PWM 4 #define PIN_MOTOR_PWM 7 #define PIN_HALL 8 // ── PWM ────────────────────────────────────────────────────── #define PWM_FREQ_MOTOR 20000 #define PWM_FREQ_LED 24989 #define PWM_RES 8 // ── Trigger ────────────────────────────────────────────────── #define TRIGGER_PULSES 16 #define IDLE_RESET_MS 5000UL #define DEBOUNCE_MS 5 // ── LED ────────────────────────────────────────────────────── #define LED_IDLE_FLASH 40 #define IDLE_FLASH_DURATION_MS 500UL #define LED_BREATHE_MIN 3 #define LED_BREATHE_MAX 40 #define LED_HELD_MIN 15 #define LED_HELD_MAX 110 #define BREATHE_PERIOD_MS 7000UL #define BREATHE_HELD_MS 3500UL #define HELD_TRANSITION_MS 800.0f // ── Motor ───────────────────────────────────────────────────── #define MOTOR_DUTY 140 #define DIAL_HELD_TIMEOUT 2000UL // ── Experience duration (TEST: 30–60 s) ────────────────────── #define EXP_MIN_MS 30000UL #define EXP_MAX_MS 60000UL // ── State machine ───────────────────────────────────────────── enum State { IDLE, EXPERIENCE, ENDING }; State state = IDLE; // ── Haptic ─────────────────────────────────────────────────── DFRobot_TM6605 TM6605; bool hapticAvailable = false; // ── Hall sensor ISR ────────────────────────────────────────── volatile bool pulseFlag = false; volatile uint32_t lastPulseUs = 0; void IRAM_ATTR onPulse() { uint32_t now = micros(); if (now - lastPulseUs > (uint32_t)DEBOUNCE_MS * 1000UL) { pulseFlag = true; lastPulseUs = now; } } // ── Experience globals ──────────────────────────────────────── unsigned long experienceDuration = 0; unsigned long experienceStart = 0; unsigned long lastPulseMs = 0; bool dialHeld = false; float breathPhase = 0.0f; float heldBlend = 0.0f; unsigned long lastLoopMs = 0; // ── Idle globals ────────────────────────────────────────────── int idlePulseCount = 0; unsigned long lastIdlePulseMs = 0; unsigned long idleFlashStartMs = 0; // ───────────────────────────────────────────────────────────── void handleIdle() { unsigned long now = millis(); #ifdef AUTO_START if (now > 5000UL) { Serial.println("[AUTO_START] Bypassing Hall sensor — entering experience."); ledcWrite(PIN_LED_PWM, 0); enterExperience(); return; } #endif if (idlePulseCount > 0 && (now - lastIdlePulseMs) > IDLE_RESET_MS) { idlePulseCount = 0; Serial.println("[IDLE] Pulse count reset."); } if (pulseFlag) { pulseFlag = false; lastIdlePulseMs = now; idleFlashStartMs = now; idlePulseCount++; Serial.printf("[IDLE] Pulse %d / %d\n", idlePulseCount, TRIGGER_PULSES); if (idlePulseCount >= TRIGGER_PULSES) { ledcWrite(PIN_LED_PWM, 0); enterExperience(); return; } } unsigned long elapsed = now - idleFlashStartMs; if (elapsed < IDLE_FLASH_DURATION_MS) { float env = sinf((float)elapsed / (float)IDLE_FLASH_DURATION_MS * (float)PI); ledcWrite(PIN_LED_PWM, (uint8_t)(env * LED_IDLE_FLASH)); } else { ledcWrite(PIN_LED_PWM, 0); } } // ───────────────────────────────────────────────────────────── void enterExperience() { Serial.println("[EXPERIENCE] Starting..."); experienceDuration = random((long)EXP_MIN_MS, (long)EXP_MAX_MS + 1); experienceStart = millis(); lastPulseMs = millis(); breathPhase = 0.0f; heldBlend = 0.0f; lastLoopMs = millis(); dialHeld = false; idlePulseCount = 0; Serial.printf("[EXPERIENCE] Duration: %lu ms\n", experienceDuration); ledcWrite(PIN_MOTOR_PWM, MOTOR_DUTY); // LED fade-in + haptic over 3000 ms int hapticStage = 0; unsigned long t0 = millis(); while (millis() - t0 < 3000UL) { unsigned long elapsed = millis() - t0; uint8_t ledDuty; if (elapsed < 1800) { ledDuty = (uint8_t)((float)elapsed / 1800.0f * 255.0f); } else { float q = (float)(elapsed - 1800) / 1200.0f; ledDuty = (uint8_t)(255.0f - q * (255.0f - LED_BREATHE_MAX)); } ledcWrite(PIN_LED_PWM, ledDuty); if (hapticAvailable && hapticStage == 0) { TM6605.selectEffect(DFRobot_TM6605::eLongSlowBoostTransition1); TM6605.play(); hapticStage = 1; } delay(4); } if (hapticAvailable) TM6605.stop(); lastLoopMs = millis(); state = EXPERIENCE; Serial.println("[EXPERIENCE] Active."); } // ───────────────────────────────────────────────────────────── void handleExperience() { unsigned long now = millis(); unsigned long dt = now - lastLoopMs; lastLoopMs = now; if (pulseFlag) { pulseFlag = false; lastPulseMs = now; } bool newDialHeld = (now - lastPulseMs > DIAL_HELD_TIMEOUT); if (newDialHeld != dialHeld) { dialHeld = newDialHeld; Serial.printf("[EXPERIENCE] dialHeld=%d\n", (int)dialHeld); } float blendTarget = dialHeld ? 1.0f : 0.0f; float blendStep = (float)dt / HELD_TRANSITION_MS; if (heldBlend < blendTarget) heldBlend = min(heldBlend + blendStep, 1.0f); else heldBlend = max(heldBlend - blendStep, 0.0f); float blendedPeriod = BREATHE_PERIOD_MS + heldBlend * ((float)BREATHE_HELD_MS - (float)BREATHE_PERIOD_MS); breathPhase += (float)TWO_PI * (float)dt / blendedPeriod; if (breathPhase > (float)TWO_PI) breathPhase -= (float)TWO_PI; float breathVal = (sinf(breathPhase) + 1.0f) / 2.0f; float ledMin = LED_BREATHE_MIN + heldBlend * (LED_HELD_MIN - LED_BREATHE_MIN); float ledMax = LED_BREATHE_MAX + heldBlend * (LED_HELD_MAX - LED_BREATHE_MAX); ledcWrite(PIN_LED_PWM, (uint8_t)(ledMin + breathVal * (ledMax - ledMin))); if (now - experienceStart >= experienceDuration) { enterEnding(); } } // ───────────────────────────────────────────────────────────── void enterEnding() { Serial.println("[ENDING] Starting..."); state = ENDING; float breathVal = (sinf(breathPhase) + 1.0f) / 2.0f; float ledMin = LED_BREATHE_MIN + heldBlend * (LED_HELD_MIN - LED_BREATHE_MIN); float ledMax = LED_BREATHE_MAX + heldBlend * (LED_HELD_MAX - LED_BREATHE_MAX); uint8_t startDuty = (uint8_t)(ledMin + breathVal * (ledMax - ledMin)); int hapticStage = 0; unsigned long t0 = millis(); while (millis() - t0 < 3100UL) { unsigned long elapsed = millis() - t0; float ledDuty; if (elapsed < 2000) { ledDuty = startDuty + ((float)elapsed / 2000.0f) * (255.0f - startDuty); } else if (elapsed < 2500) { ledDuty = 255.0f; } else { ledDuty = 255.0f * (1.0f - (float)(elapsed - 2500) / 600.0f); } ledcWrite(PIN_LED_PWM, (uint8_t)ledDuty); if (hapticAvailable) { if (hapticStage == 0) { TM6605.selectEffect(DFRobot_TM6605::eLongSlowBoostTransition1); TM6605.play(); hapticStage = 1; } else if (hapticStage == 1 && elapsed >= 2500) { TM6605.selectEffect(DFRobot_TM6605::eLongSlowFadeTransition1); TM6605.play(); hapticStage = 2; } } delay(4); } ledcWrite(PIN_LED_PWM, 0); if (hapticAvailable) TM6605.stop(); ledcWrite(PIN_MOTOR_PWM, 0); Serial.println("[ENDING] Complete. Returning to idle.\n"); idlePulseCount = 0; lastIdlePulseMs = 0; idleFlashStartMs = 0; breathPhase = 0.0f; heldBlend = 0.0f; dialHeld = false; pulseFlag = false; state = IDLE; } // ───────────────────────────────────────────────────────────── void setup() { Serial.begin(115200); delay(1500); Serial.println("\n=== INTERMISSION OBJECT — LED ONLY ==="); #ifdef AUTO_START Serial.println(" [AUTO_START: experience begins 5 s after boot]"); #endif Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL); Wire.setClock(50000); delay(500); int attempts = 0; Serial.print("TM6605 init... "); while (TM6605.begin() != 0) { Serial.println("not found, retrying..."); delay(1000); if (++attempts >= 5) { Serial.println("TM6605 failed — continuing without haptics."); break; } } if (attempts < 5) { Serial.println("ok"); hapticAvailable = true; } ledcAttach(PIN_MOTOR_PWM, PWM_FREQ_MOTOR, PWM_RES); ledcAttach(PIN_LED_PWM, PWM_FREQ_LED, PWM_RES); ledcWrite(PIN_MOTOR_PWM, 0); ledcWrite(PIN_LED_PWM, 0); pinMode(PIN_HALL, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_HALL), onPulse, FALLING); randomSeed(analogRead(0)); setupWiFiOTA(); state = IDLE; Serial.println("Ready.\n"); } // ───────────────────────────────────────────────────────────── void loop() { handleOTA(); switch (state) { case IDLE: handleIdle(); break; case EXPERIENCE: handleExperience(); break; case ENDING: break; } }