// Code generated with the help of AI from the prompt described in the documentation; manually reviewed and adapted. #include #include #include #include #include #include #include // OLED #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_SDA D4 #define OLED_SCL D5 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // LED STRIP #define LED_PIN 18 #define NUM_LEDS 106 const uint8_t BAND_BRIGHTNESS = 100; const uint8_t GOAL_BRIGHTNESS = 200; Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); // LED ZONES const int BAND1_START_A = 0; const int BAND1_END_A = 13; const int BAND1_START_B = 92; const int BAND1_END_B = 105; const int BAND2_START = 39; const int BAND2_END = 67; const int LOCAL_GOAL_START = 14; const int LOCAL_GOAL_END = 38; const int VISITOR_GOAL_START = 68; const int VISITOR_GOAL_END = 91; // BUTTONS #define BUTTON_START_STOP D0 #define BUTTON_RESET D1 // GAME CONFIG const unsigned long MATCH_DURATION_SECONDS = 180; const unsigned long RESET_HOLD_TIME = 2000; const unsigned long GOAL_MESSAGE_TIME = 1500; const unsigned long START_BLINK_TIME = 2000; const unsigned long END_RED_TIME = 5000; const unsigned long LED_BLINK_INTERVAL = 250; const unsigned long GOAL_LED_TIME = 2000; // WIFI const char* ssid = "PinSoccBall"; const char* password = "12345678"; WebServer server(80); // ESP-NOW #define EVENT_GOAL_LOCAL 1 #define EVENT_GOAL_VISITOR 2 typedef struct { uint8_t boardID; uint8_t eventType; } EspNowMessage; volatile bool espNowGoalLocalPending = false; volatile bool espNowGoalVisitorPending = false; // GAME STATE bool gameRunning = false; unsigned long remainingTime = MATCH_DURATION_SECONDS; unsigned long lastTimerUpdate = 0; int localScore = 0; int visitorScore = 0; bool showingGoal = false; unsigned long goalMessageStart = 0; String lastGoalTeam = ""; // LED STATE enum LedMode { LED_WAITING, LED_START_BLINK, LED_GAME_GREEN, LED_LAST_SECONDS_RED_BLINK, LED_END_RED, LED_OFF }; LedMode ledMode = LED_WAITING; unsigned long ledModeStart = 0; unsigned long lastBlinkTime = 0; bool blinkState = false; // GOAL EFFECT enum GoalType { GOAL_NONE, GOAL_LOCAL, GOAL_VISITOR }; GoalType currentGoalEffect = GOAL_NONE; bool goalEffectActive = false; unsigned long goalEffectStart = 0; unsigned long lastGoalBlinkTime = 0; bool goalBlinkState = false; // BUTTON STATE bool lastStartButton = HIGH; bool lastResetButton = HIGH; unsigned long resetPressStart = 0; bool resetWasPressed = false; // -------------------- COLORS -------------------- uint32_t bandGreen() { return strip.Color(0, BAND_BRIGHTNESS, 0); } uint32_t bandRed() { return strip.Color(BAND_BRIGHTNESS, 0, 0); } uint32_t goalGreen() { return strip.Color(0, GOAL_BRIGHTNESS, 0); } uint32_t goalRed() { return strip.Color(GOAL_BRIGHTNESS, 0, 0); } uint32_t offColor() { return strip.Color(0, 0, 0); } // -------------------- TIME -------------------- String formatTime(unsigned long seconds) { int minutes = seconds / 60; int secs = seconds % 60; char buffer[6]; sprintf(buffer, "%02d:%02d", minutes, secs); return String(buffer); } // -------------------- LED FUNCTIONS -------------------- void setLedRange(int startLed, int endLed, uint32_t color) { for (int i = startLed; i <= endLed; i++) { if (i >= 0 && i < NUM_LEDS) { strip.setPixelColor(i, color); } } } void setBands(uint32_t color) { setLedRange(BAND1_START_A, BAND1_END_A, color); setLedRange(BAND1_START_B, BAND1_END_B, color); setLedRange(BAND2_START, BAND2_END, color); } void setLocalGoal(uint32_t color) { setLedRange(LOCAL_GOAL_START, LOCAL_GOAL_END, color); } void setVisitorGoal(uint32_t color) { setLedRange(VISITOR_GOAL_START, VISITOR_GOAL_END, color); } void clearStrip() { strip.clear(); strip.show(); } void showBands(uint32_t color) { strip.clear(); setBands(color); strip.show(); } void setLedMode(LedMode newMode) { ledMode = newMode; ledModeStart = millis(); lastBlinkTime = millis(); blinkState = false; } void startGoalEffect(GoalType goalType) { currentGoalEffect = goalType; goalEffectActive = true; goalEffectStart = millis(); lastGoalBlinkTime = millis(); goalBlinkState = false; } void updateGoalEffect() { unsigned long now = millis(); if (now - goalEffectStart >= GOAL_LED_TIME) { goalEffectActive = false; currentGoalEffect = GOAL_NONE; if (gameRunning) { if (remainingTime <= 10) { setLedMode(LED_LAST_SECONDS_RED_BLINK); } else { setLedMode(LED_GAME_GREEN); showBands(bandGreen()); } } return; } if (now - lastGoalBlinkTime >= LED_BLINK_INTERVAL) { lastGoalBlinkTime = now; goalBlinkState = !goalBlinkState; strip.clear(); if (gameRunning) { if (remainingTime <= 10) { setBands(bandRed()); } else { setBands(bandGreen()); } } if (goalBlinkState) { if (currentGoalEffect == GOAL_LOCAL) { setLocalGoal(goalGreen()); setVisitorGoal(goalRed()); } if (currentGoalEffect == GOAL_VISITOR) { setVisitorGoal(goalGreen()); setLocalGoal(goalRed()); } } strip.show(); } } void showEndResultLeds(bool winnerBlinkOn, bool bandsBlinkOn) { strip.clear(); setBands(bandsBlinkOn ? bandRed() : offColor()); if (localScore > visitorScore) { setLocalGoal(winnerBlinkOn ? goalGreen() : offColor()); setVisitorGoal(goalRed()); } else if (visitorScore > localScore) { setVisitorGoal(winnerBlinkOn ? goalGreen() : offColor()); setLocalGoal(goalRed()); } else { setLocalGoal(winnerBlinkOn ? goalGreen() : offColor()); setVisitorGoal(winnerBlinkOn ? goalGreen() : offColor()); } strip.show(); } void updateLeds() { if (goalEffectActive) { updateGoalEffect(); return; } unsigned long now = millis(); switch (ledMode) { case LED_WAITING: break; case LED_START_BLINK: if (now - lastBlinkTime >= LED_BLINK_INTERVAL) { lastBlinkTime = now; blinkState = !blinkState; showBands(blinkState ? bandGreen() : offColor()); } if (now - ledModeStart >= START_BLINK_TIME) { setLedMode(LED_GAME_GREEN); showBands(bandGreen()); } break; case LED_GAME_GREEN: break; case LED_LAST_SECONDS_RED_BLINK: if (now - lastBlinkTime >= LED_BLINK_INTERVAL) { lastBlinkTime = now; blinkState = !blinkState; showBands(blinkState ? bandRed() : offColor()); } break; case LED_END_RED: if (now - lastBlinkTime >= LED_BLINK_INTERVAL) { lastBlinkTime = now; blinkState = !blinkState; showEndResultLeds(blinkState, blinkState); } if (now - ledModeStart >= END_RED_TIME) { resetGameAutomatic(); } break; case LED_OFF: break; } } // -------------------- DISPLAY -------------------- void updateDisplay() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); if (showingGoal) { display.setTextSize(2); display.setCursor(30, 5); display.println("GOAL"); display.setTextSize(1); display.setCursor(35, 28); display.println(lastGoalTeam); display.setTextSize(2); display.setCursor(28, 43); display.print(localScore); display.print(" - "); display.print(visitorScore); } else { display.setTextSize(1); display.setCursor(0, 0); display.println("PinSocc Ball"); display.setTextSize(2); display.setCursor(18, 16); display.print(localScore); display.print(" - "); display.print(visitorScore); display.setTextSize(2); display.setCursor(32, 40); display.println(formatTime(remainingTime)); display.setTextSize(1); display.setCursor(90, 0); display.println(gameRunning ? "RUN" : "STOP"); } display.display(); } // -------------------- GAME FUNCTIONS -------------------- void addGoalLocal() { if (!gameRunning) return; localScore++; showingGoal = true; lastGoalTeam = "LOCAL"; goalMessageStart = millis(); startGoalEffect(GOAL_LOCAL); Serial.println("GOAL LOCAL"); updateDisplay(); } void addGoalVisitor() { if (!gameRunning) return; visitorScore++; showingGoal = true; lastGoalTeam = "VISITOR"; goalMessageStart = millis(); startGoalEffect(GOAL_VISITOR); Serial.println("GOAL VISITOR"); updateDisplay(); } void toggleStartStop() { if (remainingTime == 0) { remainingTime = MATCH_DURATION_SECONDS; localScore = 0; visitorScore = 0; showingGoal = false; goalEffectActive = false; currentGoalEffect = GOAL_NONE; setLedMode(LED_WAITING); clearStrip(); } gameRunning = !gameRunning; lastTimerUpdate = millis(); if (gameRunning) { setLedMode(LED_START_BLINK); } Serial.println(gameRunning ? "GAME STARTED" : "GAME PAUSED"); updateDisplay(); } void resetGame() { if (!gameRunning) { remainingTime = MATCH_DURATION_SECONDS; localScore = 0; visitorScore = 0; showingGoal = false; goalEffectActive = false; currentGoalEffect = GOAL_NONE; setLedMode(LED_WAITING); clearStrip(); Serial.println("GAME RESET"); updateDisplay(); } } void resetGameAutomatic() { gameRunning = false; remainingTime = MATCH_DURATION_SECONDS; localScore = 0; visitorScore = 0; showingGoal = false; goalEffectActive = false; currentGoalEffect = GOAL_NONE; setLedMode(LED_WAITING); clearStrip(); Serial.println("AUTO RESET AFTER END"); updateDisplay(); } void finishGame() { gameRunning = false; remainingTime = 0; goalEffectActive = false; currentGoalEffect = GOAL_NONE; setLedMode(LED_END_RED); Serial.println("TIME FINISHED"); updateDisplay(); } // -------------------- ESP-NOW RECEIVE -------------------- void onEspNowRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) { if (len != sizeof(EspNowMessage)) { return; } EspNowMessage msg; memcpy(&msg, incomingData, sizeof(msg)); if (msg.eventType == EVENT_GOAL_LOCAL) { espNowGoalLocalPending = true; } if (msg.eventType == EVENT_GOAL_VISITOR) { espNowGoalVisitorPending = true; } } void setupEspNow() { if (esp_now_init() != ESP_OK) { Serial.println("ESP-NOW init failed"); return; } esp_now_register_recv_cb(onEspNowRecv); Serial.println("ESP-NOW receiver ready"); } // -------------------- WEB -------------------- void handleRoot() { String html = ""; html += ""; html += ""; html += ""; html += "PinSocc Ball"; html += ""; html += "

PinSocc Ball

"; html += "
"; html += String(localScore); html += " - "; html += String(visitorScore); html += "
"; html += "
"; html += formatTime(remainingTime); html += "
"; html += "
"; html += gameRunning ? "RUNNING" : "STOP"; html += "
"; html += "GOAL LOCAL"; html += "GOAL VISITOR"; html += "
"; html += "START / STOP"; html += "RESET"; html += ""; server.sendHeader("Cache-Control", "no-store"); server.send(200, "text/html", html); } void redirectHome() { server.sendHeader("Location", "/"); server.send(303); } void setupWebServer() { server.on("/", handleRoot); server.on("/goalLocal", []() { addGoalLocal(); redirectHome(); }); server.on("/goalVisitor", []() { addGoalVisitor(); redirectHome(); }); server.on("/startStop", []() { toggleStartStop(); redirectHome(); }); server.on("/reset", []() { resetGame(); redirectHome(); }); server.begin(); } // -------------------- SETUP -------------------- void setup() { Serial.begin(115200); delay(1000); pinMode(BUTTON_START_STOP, INPUT_PULLUP); pinMode(BUTTON_RESET, INPUT_PULLUP); Wire.begin(OLED_SDA, OLED_SCL); if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("OLED not found"); while (1); } strip.begin(); clearStrip(); WiFi.mode(WIFI_AP_STA); WiFi.softAP(ssid, password); setupEspNow(); Serial.println("WiFi AP started"); Serial.print("SSID: "); Serial.println(ssid); Serial.print("Password: "); Serial.println(password); Serial.print("IP: "); Serial.println(WiFi.softAPIP()); setupWebServer(); updateDisplay(); } // -------------------- LOOP -------------------- void loop() { server.handleClient(); if (espNowGoalLocalPending) { espNowGoalLocalPending = false; Serial.println("ESP-NOW RECEIVED: LOCAL GOAL"); addGoalLocal(); } if (espNowGoalVisitorPending) { espNowGoalVisitorPending = false; Serial.println("ESP-NOW RECEIVED: VISITOR GOAL"); addGoalVisitor(); } bool startButton = digitalRead(BUTTON_START_STOP); bool resetButton = digitalRead(BUTTON_RESET); if (startButton == LOW && lastStartButton == HIGH) { toggleStartStop(); delay(180); } if (!gameRunning) { if (resetButton == LOW && !resetWasPressed) { resetPressStart = millis(); resetWasPressed = true; } if (resetButton == LOW && resetWasPressed) { if (millis() - resetPressStart >= RESET_HOLD_TIME) { resetGame(); resetWasPressed = false; delay(300); } } if (resetButton == HIGH) { resetWasPressed = false; } } if (gameRunning && remainingTime > 0) { if (millis() - lastTimerUpdate >= 1000) { lastTimerUpdate = millis(); remainingTime--; if (remainingTime == 10) { setLedMode(LED_LAST_SECONDS_RED_BLINK); } if (remainingTime == 0) { finishGame(); } updateDisplay(); } } if (showingGoal && millis() - goalMessageStart >= GOAL_MESSAGE_TIME) { showingGoal = false; updateDisplay(); } updateLeds(); lastStartButton = startButton; lastResetButton = resetButton; }