/* GameOnSign — minimal ESP32 "game on" relay example --------------------------------------------------- Polls https://gameonsign.com/api/v1/live.php for one team and drives a relay: * flashes the sign a few times at T-5 min and T-2 min before the start (sign.flash_at) * ON from the start until the data source confirms FINAL (game_on stays true through intermissions, halftime, rain delays and overtime) * if the network drops, it keeps running from the last known schedule (sign.on_at .. sign.est_off) until the API is reachable again Libraries: ESP32 Arduino core (WiFi, HTTPClient), ArduinoJson 7.x Board: any ESP32 / ESP32-S3. Wire a relay module input to RELAY_PIN. */ #include #include #include #include // ---------------- user settings ---------------- const char* WIFI_SSID = ""; // your Wi-Fi const char* WIFI_PASS = ""; const char* API_HOST = "gameonsign.com"; const char* LEAGUE = "nhl"; // nhl | nfl | nba | mlb const char* TEAM = "TBL"; // see /api/v1/teams.php?league=nhl const int RELAY_PIN = 26; // SPORTS_LIGHT board uses GPIO6 const bool RELAY_ACTIVE_LOW = true; // most cheap relay modules switch on LOW const int FLASH_SECONDS = 20; // length of each pre-game flash burst const int FLASH_PERIOD_MS = 500; // on/off blink period during a burst // ------------------------------------------------ struct SignState { bool valid = false; bool gameOn = false; bool offConfirmed = false; String state = "none"; uint32_t flashAt[2] = {0, 0}; uint32_t onAt = 0, estOff = 0; uint32_t pollSec = 60; } sign; String etag; uint32_t lastOkMs = 0, nextPollMs = 0; void relay(bool on) { digitalWrite(RELAY_PIN, (on ^ RELAY_ACTIVE_LOW) ? HIGH : LOW); } time_t nowUtc() { return time(nullptr); } bool timeValid() { return nowUtc() > 1700000000; } bool fetchLive() { String url = String("http://") + API_HOST + "/api/v1/live.php?league=" + LEAGUE + "&team=" + TEAM; WiFiClient client; // plain HTTP keeps RAM use low; use WiFiClientSecure for HTTPS HTTPClient http; http.useHTTP10(true); // no chunked encoding, stream straight into ArduinoJson http.setTimeout(8000); if (!http.begin(client, url)) return false; const char* keys[] = {"ETag"}; http.collectHeaders(keys, 1); if (etag.length()) http.addHeader("If-None-Match", etag); int code = http.GET(); if (code == 304) { http.end(); return true; } // nothing changed if (code != 200) { Serial.printf("[api] HTTP %d\n", code); http.end(); return false; } etag = http.header("ETag"); JsonDocument doc; DeserializationError err = deserializeJson(doc, http.getStream()); http.end(); if (err) { Serial.printf("[api] JSON %s\n", err.c_str()); return false; } sign.valid = true; sign.state = doc["state"] | "none"; sign.gameOn = doc["game_on"] | false; sign.offConfirmed = doc["sign"]["off_confirmed"] | false; sign.flashAt[0] = doc["sign"]["flash_at"][0] | 0; sign.flashAt[1] = doc["sign"]["flash_at"][1] | 0; sign.onAt = doc["sign"]["on_at"] | 0; sign.estOff = doc["sign"]["est_off"] | 0; sign.pollSec = constrain((int)(doc["poll"] | 60), 15, 300); Serial.printf("[api] %s game_on=%d %s %d-%d %s %s\n", sign.state.c_str(), sign.gameOn, (const char*)(doc["opp"] | ""), (int)(doc["score"]["team"] | 0), (int)(doc["score"]["opp"] | 0), (const char*)(doc["period"] | ""), (const char*)(doc["clock"] | "")); return true; } // Decide what the relay should do right now. bool relayShouldBeOn() { if (!sign.valid || !timeValid()) return false; uint32_t t = nowUtc(); bool online = millis() - lastOkMs < 180000; // fresh data within 3 minutes bool on = online ? sign.gameOn : (!sign.offConfirmed && sign.onAt && t >= sign.onAt && t < sign.estOff); // offline: schedule if (on) return true; for (int i = 0; i < 2; i++) { // T-5 / T-2 flash bursts if (sign.flashAt[i] && t >= sign.flashAt[i] && t < sign.flashAt[i] + FLASH_SECONDS) return (millis() / FLASH_PERIOD_MS) % 2 == 0; } return false; } void setup() { Serial.begin(115200); pinMode(RELAY_PIN, OUTPUT); relay(false); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); configTime(0, 0, "pool.ntp.org", "time.nist.gov"); // UTC; the API uses epoch seconds } void loop() { if (WiFi.status() == WL_CONNECTED && (int32_t)(millis() - nextPollMs) >= 0) { bool ok = fetchLive(); if (ok) lastOkMs = millis(); nextPollMs = millis() + (ok ? sign.pollSec : 30) * 1000UL; // honor the server's poll hint } relay(relayShouldBeOn()); delay(50); }