#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <HTTPClient.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define SDA_PIN 19
#define SCL_PIN 18
#define BUTTON_PIN 4
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* vercelApiUrl = "https://project-hjckp.vercel.app/api/trigger-dracula";
enum PlayerState { IDLE, WAITING, PLAYING };
volatile PlayerState state = IDLE;
volatile bool requestDone = false;
volatile bool requestOK = false;
unsigned long songStartTime = 0;
/* ---------------- LYRIC TIMELINE ---------------- */
struct LyricWord {
const char* word;
unsigned long startMs;
unsigned long endMs;
int animType;
};
const LyricWord timeline[] = {
{"The",0,250,0},{"morning",250,500,1},{"light",500,750,0},{"is",750,1000,1},
{"turning",1000,1250,0},{"blue,",1250,1500,1},{"the",1500,1800,0},{"feeling",1800,2100,1},
{"is",2100,2400,0},{"BIZARRE",2400,4000,3},{"The",4000,4300,0},{"night",4300,4600,1},
{"is",4600,4900,0},{"almost",4900,5200,1},{"over,",5200,5500,0},{"I",5500,5800,0},
{"still",5800,6200,2},{"don't",6200,6600,0},{"know",6600,7000,1},{"where",7000,7500,2},
{"you",7500,8000,0},{"are.",8000,8500,1},{"",8500,9500,4},{"The",9500,10000,0},
{"shadows,",10000,10500,1},{"yeah,",10500,11000,3},{"they",11000,11200,0},{"keep",11200,11400,1},
{"me",11400,11600,0},{"pretty",11600,12000,2},{"like",12000,12200,0},{"a",12200,12400,1},
{"movie",12400,12700,0},{"star.",12700,13000,1},{"Daylight",13000,13375,0},{"makes",13375,13750,1},
{"me",13750,14125,0},{"feel",14125,14500,3},{"like",14500,15000,0},{"DRACULA",15000,18000,3},
{"",18000,19000,4}
};
const int wordCount = sizeof(timeline) / sizeof(timeline[0]);
/* ---------------- DISPLAY HELPERS ---------------- */
void renderCentered(const char* txt, int size, int ox=0, int oy=0) {
int16_t x1,y1; uint16_t w,h;
display.setTextSize(size);
display.setTextColor(SSD1306_WHITE);
display.getTextBounds(txt,0,0,&x1,&y1,&w,&h);
display.setCursor((SCREEN_WIDTH-w)/2 + ox, (SCREEN_HEIGHT-h)/2 + oy);
display.print(txt);
}
void showIdle() {
display.clearDisplay();
display.drawRect(0,0,SCREEN_WIDTH,SCREEN_HEIGHT,SSD1306_WHITE);
renderCentered("// READY",1,0,-8);
renderCentered("PRESS BUTTON",1,0,10);
display.display();
}
void showWaiting() {
static uint8_t dots=0;
static unsigned long last=0;
if (millis()-last>250) { dots=(dots+1)%4; last=millis(); }
display.clearDisplay();
display.drawRect(0,0,SCREEN_WIDTH,SCREEN_HEIGHT,SSD1306_WHITE);
renderCentered("WAITING",2,0,-5);
display.setCursor(55,52);
for(int i=0;i<dots;i++) display.print(".");
display.display();
}
/* ---------------- NETWORK TASK ---------------- */
void networkTask(void *p) {
requestDone=false;
requestOK=false;
if (WiFi.status()==WL_CONNECTED) {
HTTPClient http;
http.setReuse(true);
http.setConnectTimeout(800);
http.setTimeout(800);
http.begin(vercelApiUrl);
http.addHeader("Content-Type","application/json");
int code = http.POST("{\"trigger\":\"dracula\"}");
if (code>=200 && code<300) requestOK=true;
http.end();
}
requestDone=true;
vTaskDelete(NULL);
}
/* ---------------- ANIMATION ---------------- */
void drawFrame(unsigned long t) {
display.clearDisplay();
int idx=-1;
for(int i=0;i<wordCount;i++){
if(t>=timeline[i].startMs && t<timeline[i].endMs){ idx=i; break; }
}
if(idx<0){ display.display(); return; }
auto w = timeline[idx];
float p = float(t-w.startMs)/float(w.endMs-w.startMs);
switch(w.animType){
case 0:
renderCentered(w.word,(p<0.6)?1:2);
break;
case 1:
display.drawFastVLine(p*60,10,40,SSD1306_WHITE);
display.drawFastVLine(128-(p*60),10,40,SSD1306_WHITE);
renderCentered(w.word,2);
break;
case 2:
display.drawCircle(64,32,8+sin(p*3.14)*12,SSD1306_WHITE);
renderCentered(w.word,2);
break;
case 3:
renderCentered(w.word,2,random(-2,3),random(-2,3));
break;
case 4:
for(int i=0;i<20;i++)
display.drawPixel(random(128),random(64),SSD1306_WHITE);
break;
}
display.display();
}
/* ---------------- SETUP ---------------- */
void setup() {
pinMode(BUTTON_PIN,INPUT_PULLUP);
Wire.begin(SDA_PIN,SCL_PIN);
display.begin(SSD1306_SWITCHCAPVCC,0x3C);
display.clearDisplay();
renderCentered("CONNECTING",1);
display.display();
WiFi.setSleep(false);
WiFi.begin(ssid,password);
while(WiFi.status()!=WL_CONNECTED) delay(100);
showIdle();
}
/* ---------------- LOOP ---------------- */
void loop() {
switch(state){
case IDLE:
if(digitalRead(BUTTON_PIN)==LOW){
delay(20);
if(digitalRead(BUTTON_PIN)==LOW){
state = WAITING;
xTaskCreatePinnedToCore(networkTask,"net",4096,NULL,1,NULL,0);
}
}
break;
case WAITING:
showWaiting();
if(requestDone){
if(requestOK){
songStartTime=millis();
state=PLAYING;
}else{
state=IDLE;
showIdle();
}
}
break;
case PLAYING:{
unsigned long e=millis()-songStartTime;
if(e>=19000){
state=IDLE;
showIdle();
}else{
drawFrame(e);
}
} break;
}
}