#include <Adafruit_ILI9341.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <SPI.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
constexpr char kWifiName[] = "Wokwi-GUEST";
constexpr char kFavoriteTeam[] = "TEN";
constexpr int kDisplayCsPin = 15;
constexpr int kDisplayDcPin = 2;
constexpr int kPreviousButtonPin = 32;
constexpr int kSelectButtonPin = 33;
constexpr int kNextButtonPin = 25;
constexpr unsigned long kRefreshIntervalMs = 60000;
constexpr char kNflScoreboardUrl[] =
"https://site.api.espn.com/apis/site/v2/sports/"
"football/nfl/scoreboard?limit=5";
Adafruit_ILI9341 display(
kDisplayCsPin,
kDisplayDcPin);
struct Game {
String away;
String home;
int awayScore = 0;
int homeScore = 0;
String status;
String detail;
};
Game games[5];
int gameCount = 0;
bool autoMode = true;
int manualScreen = 0;
int favoriteGameIndex = -1;
bool previousHeld = false;
bool selectHeld = false;
bool nextHeld = false;
unsigned long lastFetchMs = 0;
bool hadFavoriteScore = false;
int oldFavoriteAwayScore = 0;
int oldFavoriteHomeScore = 0;
bool buttonPressed(int pin, bool& wasHeld) {
const bool held = digitalRead(pin) == LOW;
const bool pressed = held && !wasHeld;
wasHeld = held;
return pressed;
}
void drawHeader(const char* title) {
display.fillRect(0, 0, 320, 36, ILI9341_BLUE);
display.setTextColor(ILI9341_WHITE);
display.setTextSize(2);
display.setCursor(10, 10);
display.print(title);
}
void drawMessage(const char* title, const char* message) {
display.fillScreen(ILI9341_BLACK);
drawHeader(title);
display.setTextColor(ILI9341_WHITE);
display.setTextSize(2);
display.setCursor(16, 80);
display.print(message);
}
void drawScoreboard() {
display.fillScreen(ILI9341_BLACK);
drawHeader("INKZONE NFL");
display.setTextColor(ILI9341_LIGHTGREY);
display.setTextSize(1);
display.setCursor(12, 45);
display.print(autoMode ? "AUTO MODE" : "MANUAL MODE");
int rowY = 64;
for (int index = 0; index < gameCount; ++index) {
const Game& game = games[index];
const bool favorite =
game.away == kFavoriteTeam ||
game.home == kFavoriteTeam;
display.drawRect(
8, rowY - 8, 304, 31,
favorite ? ILI9341_YELLOW : ILI9341_DARKGREY);
display.setTextColor(
favorite ? ILI9341_YELLOW : ILI9341_WHITE);
display.setTextSize(2);
display.setCursor(18, rowY);
display.printf(
"%s %d @ %s %d",
game.away.c_str(),
game.awayScore,
game.home.c_str(),
game.homeScore);
display.setTextSize(1);
display.setCursor(222, rowY + 17);
display.print(game.status);
rowY += 37;
}
if (gameCount == 0) {
display.setTextColor(ILI9341_WHITE);
display.setTextSize(2);
display.setCursor(18, 85);
display.print("No NFL games found");
}
}
void drawFavoriteGame() {
if (favoriteGameIndex < 0) {
drawMessage(
"FAVORITE TEAM",
"TEN is not in this feed");
return;
}
const Game& game = games[favoriteGameIndex];
display.fillScreen(ILI9341_BLACK);
drawHeader("TENNESSEE TITANS");
display.setTextColor(ILI9341_LIGHTGREY);
display.setTextSize(2);
display.setCursor(82, 62);
display.print(game.status);
display.setTextColor(ILI9341_WHITE);
display.setTextSize(4);
display.setCursor(26, 112);
display.printf(
"%s @ %s",
game.away.c_str(),
game.home.c_str());
display.setTextColor(ILI9341_YELLOW);
display.setTextSize(4);
display.setCursor(84, 164);
display.printf(
"%d - %d",
game.awayScore,
game.homeScore);
display.setTextColor(ILI9341_LIGHTGREY);
display.setTextSize(1);
display.setCursor(18, 214);
display.print(game.detail);
}
void drawDashboard() {
display.fillScreen(ILI9341_BLACK);
drawHeader("INKZONE DASHBOARD");
display.setTextColor(ILI9341_YELLOW);
display.setTextSize(2);
display.setCursor(18, 60);
display.print("FAVORITE: TEN");
display.setTextColor(ILI9341_LIGHTGREY);
display.setTextSize(1);
display.setCursor(18, 96);
display.print("NEXT TITANS GAME");
display.setTextColor(ILI9341_WHITE);
display.setTextSize(3);
display.setCursor(18, 120);
if (favoriteGameIndex >= 0) {
const Game& game = games[favoriteGameIndex];
display.printf(
"%s @ %s",
game.away.c_str(),
game.home.c_str());
display.setTextSize(2);
display.setCursor(80, 170);
display.print(game.status);
} else {
display.print("No game found");
}
display.setTextColor(ILI9341_LIGHTGREY);
display.setTextSize(1);
display.setCursor(18, 218);
display.print("LIVE DATA REFRESHES EVERY 60 SEC");
}
void drawScoreAlert(const Game& game, bool TitansScored) {
for (int flash = 0; flash < 2; ++flash) {
display.fillScreen(ILI9341_BLUE);
delay(160);
display.fillScreen(ILI9341_BLACK);
delay(110);
}
display.fillScreen(ILI9341_BLUE);
display.setTextColor(ILI9341_WHITE);
display.setTextSize(4);
display.setCursor(20, 42);
display.print("SCORE!");
display.setTextColor(ILI9341_YELLOW);
display.setTextSize(4);
display.setCursor(72, 106);
display.print(TitansScored ? "TEN" : "OPP");
display.setTextColor(ILI9341_WHITE);
display.setTextSize(3);
display.setCursor(78, 168);
display.printf(
"%d - %d",
game.awayScore,
game.homeScore);
delay(1200);
}
void drawCurrentScreen() {
if (autoMode) {
if (favoriteGameIndex >= 0 &&
games[favoriteGameIndex].status == "LIVE") {
drawFavoriteGame();
} else {
drawScoreboard();
}
return;
}
if (manualScreen == 0) {
drawScoreboard();
} else if (manualScreen == 1) {
drawFavoriteGame();
} else {
drawDashboard();
}
}
String gameStatus(JsonObjectConst status) {
const bool completed =
status["type"]["completed"] | false;
const char* state =
status["type"]["state"] | "";
if (completed) {
return "FINAL";
}
if (String(state) == "in") {
return "LIVE";
}
return "UPCOMING";
}
bool fetchNflScoreboard() {
Serial.println("Requesting NFL scoreboard...");
if (WiFi.status() != WL_CONNECTED) {
Serial.println("NFL error: Wi-Fi is not connected");
return false;
}
WiFiClientSecure client;
client.setInsecure();
HTTPClient request;
if (!request.begin(client, kNflScoreboardUrl)) {
Serial.println("NFL error: request.begin() failed");
return false;
}
const int statusCode = request.GET();
Serial.printf("NFL HTTP status: %d\n", statusCode);
if (statusCode != HTTP_CODE_OK) {
request.end();
Serial.println("NFL error: ESPN request failed");
return false;
}
const String json = request.getString();
request.end();
Serial.printf(
"NFL downloaded: %u bytes\n",
static_cast<unsigned int>(json.length()));
if (json.isEmpty()) {
Serial.println("NFL error: empty response");
return false;
}
JsonDocument filter;
filter["events"][0]["id"] = true;
filter["events"][0]["date"] = true;
JsonObject statusFilter =
filter["events"][0]["status"].to<JsonObject>();
statusFilter["period"] = true;
statusFilter["displayClock"] = true;
statusFilter["type"]["state"] = true;
statusFilter["type"]["completed"] = true;
JsonObject competitorFilter =
filter["events"][0]["competitions"][0]
["competitors"][0]
.to<JsonObject>();
competitorFilter["homeAway"] = true;
competitorFilter["score"] = true;
competitorFilter["team"]["id"] = true;
competitorFilter["team"]["displayName"] = true;
competitorFilter["team"]["abbreviation"] = true;
competitorFilter["team"]["color"] = true;
competitorFilter["team"]["alternateColor"] = true;
JsonDocument document;
const DeserializationError error =
deserializeJson(
document,
json,
DeserializationOption::Filter(filter),
DeserializationOption::NestingLimit(20));
if (error) {
Serial.printf(
"NFL JSON error: %s\n",
error.c_str());
return false;
}
const JsonArrayConst events =
document["events"].as<JsonArrayConst>();
Serial.printf(
"NFL events found: %u\n",
static_cast<unsigned int>(events.size()));
if (events.isNull() || events.size() == 0) {
Serial.println("NFL error: events array is empty");
return false;
}
Game parsedGames[5];
int parsedGameCount = 0;
int parsedFavoriteIndex = -1;
for (JsonObjectConst event : events) {
if (parsedGameCount >= 5) {
break;
}
const JsonArrayConst competitions =
event["competitions"].as<JsonArrayConst>();
if (competitions.isNull() || competitions.size() == 0) {
continue;
}
const JsonArrayConst competitors =
competitions[0]["competitors"]
.as<JsonArrayConst>();
if (competitors.isNull() ||
competitors.size() < 2) {
continue;
}
Game game;
const JsonObjectConst status = event["status"];
game.status = gameStatus(status);
for (JsonObjectConst competitor : competitors) {
const bool isHome =
strcmp(
competitor["homeAway"] | "",
"home") == 0;
const String abbreviation =
competitor["team"]["abbreviation"] | "???";
const char* scoreText =
competitor["score"] | "0";
const int score = String(scoreText).toInt();
if (isHome) {
game.home = abbreviation;
game.homeScore = score;
} else {
game.away = abbreviation;
game.awayScore = score;
}
}
if (game.home == "???" || game.away == "???") {
continue;
}
parsedGames[parsedGameCount] = game;
if (game.home == kFavoriteTeam ||
game.away == kFavoriteTeam) {
parsedFavoriteIndex = parsedGameCount;
}
Serial.printf(
"Parsed NFL game: %s %d at %s %d\n",
game.away.c_str(),
game.awayScore,
game.home.c_str(),
game.homeScore);
++parsedGameCount;
}
if (parsedGameCount == 0) {
Serial.println("NFL error: no valid games parsed");
return false;
}
gameCount = parsedGameCount;
favoriteGameIndex = parsedFavoriteIndex;
for (int index = 0; index < gameCount; ++index) {
games[index] = parsedGames[index];
}
if (favoriteGameIndex >= 0) {
const Game& favoriteGame =
games[favoriteGameIndex];
if (hadFavoriteScore &&
(favoriteGame.homeScore > oldFavoriteHomeScore ||
favoriteGame.awayScore > oldFavoriteAwayScore)) {
}
oldFavoriteHomeScore = favoriteGame.homeScore;
oldFavoriteAwayScore = favoriteGame.awayScore;
hadFavoriteScore = true;
} else {
hadFavoriteScore = false;
}
Serial.printf(
"NFL games saved: %d, favorite index: %d\n",
gameCount,
favoriteGameIndex);
return true;
}
void connectToWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(kWifiName, "", 6);
display.fillScreen(ILI9341_BLACK);
drawHeader("INKZONE NFL");
display.setTextColor(ILI9341_WHITE);
display.setTextSize(2);
display.setCursor(18, 80);
display.print("Connecting to Wi-Fi...");
while (WiFi.status() != WL_CONNECTED) {
delay(150);
}
}
void setup() {
Serial.begin(115200);
pinMode(kPreviousButtonPin, INPUT_PULLUP);
pinMode(kSelectButtonPin, INPUT_PULLUP);
pinMode(kNextButtonPin, INPUT_PULLUP);
display.begin();
display.setRotation(1);
display.setTextWrap(false);
connectToWifi();
if (!fetchNflScoreboard()) {
drawMessage(
"INKZONE NFL",
"Live data unavailable");
} else {
drawCurrentScreen();
}
lastFetchMs = millis();
}
void loop() {
if (buttonPressed(
kPreviousButtonPin,
previousHeld)) {
autoMode = false;
manualScreen = (manualScreen + 2) % 3;
drawCurrentScreen();
}
if (buttonPressed(
kNextButtonPin,
nextHeld)) {
autoMode = false;
manualScreen = (manualScreen + 1) % 3;
drawCurrentScreen();
}
if (buttonPressed(
kSelectButtonPin,
selectHeld)) {
autoMode = true;
drawCurrentScreen();
}
if (millis() - lastFetchMs >=
kRefreshIntervalMs) {
fetchNflScoreboard();
drawCurrentScreen();
lastFetchMs = millis();
}
}