#include #include #include #include "SD_Card.h" #include "Display_ST7789.h" #include "LVGL_Driver.h" #include "LVGL_Example.h" extern const lv_font_t lv_font_montserrat_22; extern const lv_font_t lv_font_montserrat_48; // ---------- LED STRIP ---------- #define LED_PIN 0 #define TOTAL_LEDS 120 #define LOCAL_START 0 #define LOCAL_END 59 #define VISITOR_START 60 #define VISITOR_END 119 Adafruit_NeoPixel strip(TOTAL_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); // ---------- DATA STRUCT ---------- typedef struct struct_message { bool goalLocal; bool goalVisitor; bool startStop; bool reset; } struct_message; struct_message data; // ---------- LVGL OBJECTS ---------- lv_obj_t *timeLabel; lv_obj_t *goalLabel; lv_obj_t *scoreLabel; lv_obj_t *statusLabel; // ---------- MATCH CONTROL ---------- const unsigned long matchDurationMs = 20000; // 30 seconds unsigned long remainingTimeMs = matchDurationMs; bool timerRunning = false; bool timeFinished = false; unsigned long lastTick = 0; int localScore = 0; int visitorScore = 0; // ---------- GOAL DISPLAY ---------- bool goalActive = false; unsigned long goalTime = 0; const unsigned long goalDuration = 1000; // ---------- GOAL LED EFFECT ---------- bool ledGoalActive = false; bool ledGoalLocal = false; bool ledGoalVisitor = false; unsigned long ledGoalStart = 0; const unsigned long ledGoalDuration = 2000; // ---------- START MATCH EFFECT ---------- bool startEffectActive = false; unsigned long startEffectStart = 0; const unsigned long startEffectDuration = 2000; // ---------- FINAL WARNING EFFECT ---------- bool finalWarningActive = false; // ---------- BLINK CONTROL ---------- unsigned long lastBlinkTime = 0; bool blinkState = false; const int blinkInterval = 150; // ---------- EVENT FLAGS ---------- volatile bool eventGoalLocal = false; volatile bool eventGoalVisitor = false; volatile bool eventStartStop = false; volatile bool eventReset = false; // ---------- LED HELPERS ---------- void clearStripBuffer() { for (int i = 0; i < TOTAL_LEDS; i++) { strip.setPixelColor(i, 0); } } void setSegmentColorBuffer(int startLed, int endLed, uint8_t r, uint8_t g, uint8_t b) { for (int i = startLed; i <= endLed; i++) { strip.setPixelColor(i, strip.Color(r, g, b)); } } void setAllColorBuffer(uint8_t r, uint8_t g, uint8_t b) { for (int i = 0; i < TOTAL_LEDS; i++) { strip.setPixelColor(i, strip.Color(r, g, b)); } } void renderLeds() { clearStripBuffer(); // ---------- PRIORITY 1: START EFFECT ---------- if (startEffectActive) { if (blinkState) { setAllColorBuffer(0, 255, 0); } } // ---------- BASE LAYER ---------- else if (timeFinished) { setAllColorBuffer(255, 0, 0); } else if (finalWarningActive && timerRunning) { if (blinkState) { setAllColorBuffer(255, 0, 0); } } // ---------- OVERLAY LAYER ---------- if (!startEffectActive && ledGoalActive && blinkState) { if (ledGoalLocal) { setSegmentColorBuffer(LOCAL_START, LOCAL_END, 0, 255, 0); } if (ledGoalVisitor) { setSegmentColorBuffer(VISITOR_START, VISITOR_END, 0, 255, 0); } } strip.show(); } // ---------- UI HELPERS ---------- void updateTimeDisplay() { unsigned long totalSeconds = remainingTimeMs / 1000; unsigned long minutes = totalSeconds / 60; unsigned long seconds = totalSeconds % 60; char timeText[10]; sprintf(timeText, "%02lu:%02lu", minutes, seconds); lv_label_set_text(timeLabel, timeText); } void updateScoreDisplay() { char scoreText[16]; sprintf(scoreText, "%d - %d", localScore, visitorScore); lv_label_set_text(scoreLabel, scoreText); } void showMACScreen() { lv_obj_clean(lv_scr_act()); lv_obj_set_style_bg_color(lv_scr_act(), lv_color_white(), 0); lv_obj_t *label = lv_label_create(lv_scr_act()); String macText = "WAVESHARE\nMAC ADDRESS\n\n" + WiFi.macAddress(); lv_label_set_text(label, macText.c_str()); lv_obj_set_style_text_font(label, &lv_font_montserrat_22, 0); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); delay(3000); } void createUI() { lv_obj_clean(lv_scr_act()); lv_obj_set_style_bg_color(lv_scr_act(), lv_color_white(), 0); timeLabel = lv_label_create(lv_scr_act()); lv_label_set_text(timeLabel, "00:30"); lv_obj_set_style_text_font(timeLabel, &lv_font_montserrat_48, 0); lv_obj_align(timeLabel, LV_ALIGN_TOP_MID, 0, 10); goalLabel = lv_label_create(lv_scr_act()); lv_label_set_text(goalLabel, ""); lv_obj_set_style_text_font(goalLabel, &lv_font_montserrat_48, 0); lv_obj_align(goalLabel, LV_ALIGN_CENTER, 0, -20); scoreLabel = lv_label_create(lv_scr_act()); lv_label_set_text(scoreLabel, "0 - 0"); lv_obj_set_style_text_font(scoreLabel, &lv_font_montserrat_48, 0); lv_obj_align(scoreLabel, LV_ALIGN_CENTER, 0, 45); statusLabel = lv_label_create(lv_scr_act()); lv_label_set_text(statusLabel, "Stopped"); lv_obj_set_style_text_font(statusLabel, &lv_font_montserrat_22, 0); lv_obj_align(statusLabel, LV_ALIGN_BOTTOM_MID, 0, -10); updateTimeDisplay(); updateScoreDisplay(); } // ---------- RECEIVE CALLBACK ---------- void onReceive(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) { memcpy(&data, incomingData, sizeof(data)); if (data.goalLocal) eventGoalLocal = true; if (data.goalVisitor) eventGoalVisitor = true; if (data.startStop) eventStartStop = true; if (data.reset) eventReset = true; } // ---------- SETUP ---------- void setup() { Serial.begin(115200); delay(2000); Serial.println("Booting Waveshare scoreboard..."); Flash_test(); LCD_Init(); Lvgl_Init(); SD_Init(); strip.begin(); strip.clear(); strip.show(); WiFi.mode(WIFI_STA); WiFi.disconnect(); Serial.print("Waveshare MAC: "); Serial.println(WiFi.macAddress()); showMACScreen(); createUI(); if (esp_now_init() != ESP_OK) { Serial.println("ESP-NOW init failed"); lv_label_set_text(statusLabel, "ESP-NOW error"); return; } esp_now_register_recv_cb(onReceive); Serial.println("ESP-NOW receiver ready"); lv_label_set_text(statusLabel, "Stopped"); } // ---------- LOOP ---------- void loop() { Timer_Loop(); unsigned long currentMillis = millis(); // ---------- START / STOP ---------- if (eventStartStop) { eventStartStop = false; if (!timeFinished) { timerRunning = !timerRunning; if (timerRunning) { lastTick = currentMillis; lv_label_set_text(statusLabel, "Running"); Serial.println("TIMER START"); if (remainingTimeMs == matchDurationMs) { startEffectActive = true; startEffectStart = currentMillis; } } else { lv_label_set_text(statusLabel, "Stopped"); Serial.println("TIMER STOP"); } } } // ---------- RESET ---------- if (eventReset) { eventReset = false; if (!timerRunning) { remainingTimeMs = matchDurationMs; localScore = 0; visitorScore = 0; timeFinished = false; goalActive = false; ledGoalActive = false; ledGoalLocal = false; ledGoalVisitor = false; finalWarningActive = false; startEffectActive = false; updateTimeDisplay(); updateScoreDisplay(); lv_label_set_text(goalLabel, ""); lv_label_set_text(statusLabel, "Reset"); Serial.println("RESET OK"); } else { Serial.println("RESET IGNORED (timer running)"); } } // ---------- GOAL LOCAL ---------- if (eventGoalLocal) { eventGoalLocal = false; if (timerRunning && !timeFinished) { localScore++; updateScoreDisplay(); goalActive = true; goalTime = currentMillis; lv_label_set_text(goalLabel, "GOAL"); ledGoalActive = true; ledGoalLocal = true; ledGoalVisitor = false; ledGoalStart = currentMillis; Serial.print("GOAL LOCAL -> Score: "); Serial.print(localScore); Serial.print(" - "); Serial.println(visitorScore); } else { Serial.println("GOAL LOCAL IGNORED (timer not running)"); } } // ---------- GOAL VISITOR ---------- if (eventGoalVisitor) { eventGoalVisitor = false; if (timerRunning && !timeFinished) { visitorScore++; updateScoreDisplay(); goalActive = true; goalTime = currentMillis; lv_label_set_text(goalLabel, "GOAL"); ledGoalActive = true; ledGoalLocal = false; ledGoalVisitor = true; ledGoalStart = currentMillis; Serial.print("GOAL VISITOR -> Score: "); Serial.print(localScore); Serial.print(" - "); Serial.println(visitorScore); } else { Serial.println("GOAL VISITOR IGNORED (timer not running)"); } } // ---------- TIMER UPDATE ---------- if (timerRunning && !timeFinished) { if (currentMillis - lastTick >= 1000) { lastTick += 1000; if (remainingTimeMs >= 1000) { remainingTimeMs -= 1000; } else { remainingTimeMs = 0; } updateTimeDisplay(); if (remainingTimeMs <= 10000 && remainingTimeMs > 0) { finalWarningActive = true; } else { finalWarningActive = false; } if (remainingTimeMs == 0) { timerRunning = false; timeFinished = true; finalWarningActive = false; lv_label_set_text(statusLabel, "Finished"); Serial.println("TIME FINISHED"); } } } // ---------- GOAL TEXT TIMER ---------- if (goalActive && (currentMillis - goalTime > goalDuration)) { goalActive = false; lv_label_set_text(goalLabel, ""); } // ---------- GOAL LED TIMER ---------- if (ledGoalActive && (currentMillis - ledGoalStart > ledGoalDuration)) { ledGoalActive = false; ledGoalLocal = false; ledGoalVisitor = false; } // ---------- START EFFECT TIMER ---------- if (startEffectActive && (currentMillis - startEffectStart > startEffectDuration)) { startEffectActive = false; } // ---------- COMMON BLINK CLOCK ---------- if (currentMillis - lastBlinkTime > blinkInterval) { lastBlinkTime = currentMillis; blinkState = !blinkState; } // ---------- RENDER LEDS ---------- renderLeds(); delay(5); }