-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Description
I'm building an NFL team stats display using an ESP32 with TFT display. The device fetches game schedule data from ESPN's API (http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams/den/schedule).
The JSON response is approximately 100KB+ and causes ArduinoJson to fail with "InvalidInput" error even with 64KB buffer allocation. JSON filters also fail to work properly - even with proper filter syntax, the parsed document contains 0 events despite the raw JSON containing multiple game events. The goal is to extract team record, next game opponent, date/time, and venue information.
Troubleshooter's report
- The program uses ArduinoJson 7
- The issue happens at run time
- The issue concerns serialization
- Output is empty
JsonDocument::overflowed()returnsfalse
Environment
- Microcontroller: ESP32-DevKitC-32
- Core/Framework: esp32 by Espressif Systems v3.2.1
- IDE: Arduino IDE 2.3.6
Reproduction code
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
void setup() {
Serial.begin(115200);
WiFi.begin("SSID", "PASSWORD");
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void loop() {
HTTPClient http;
http.begin("http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams/den/schedule");
if (http.GET() == 200) {
// Create filter using .add<JsonObject>() syntax from ArduinoJson Assistant
JsonDocument filter;
filter["team"]["recordSummary"] = true;
JsonObject filter_events_0 = filter["events"].add<JsonObject>();
filter_events_0["date"] = true;
filter_events_0["week"]["number"] = true;
JsonObject filter_events_0_competitions_0 = filter_events_0["competitions"].add<JsonObject>();
filter_events_0_competitions_0["status"]["type"]["state"] = true;
JsonObject filter_events_0_competitions_0_competitors_0 = filter_events_0_competitions_0["competitors"].add<JsonObject>();
filter_events_0_competitions_0_competitors_0["team"]["displayName"] = true;
filter["byeWeek"] = true;
// Parse with filter
Stream& stream = http.getStream();
JsonDocument doc;
DeserializationError error = deserializeJson(doc, stream, DeserializationOption::Filter(filter));
Serial.print("Error: ");
Serial.println(error.c_str());
Serial.print("Memory used: ");
Serial.println(doc.memoryUsage()); // Shows 0
Serial.print("Events found: ");
Serial.println(doc["events"].size()); // Expected: 17, Actual: 0
}
http.end();
delay(60000);
}Remarks
The raw JSON payload can be retrieved successfully and is valid JSON
Other smaller API endpoints parse correctly with the same code
Increasing buffer size beyond 65KB causes memory allocation failures
This appears to be a common issue with large sports API responses on ESP32
Looking for suggestions on streaming JSON parsing or alternative approaches for handling large API responses with limited memory
full code below:
#include <TFT_eSPI.h>
#include "mode_1.h"
#include "mode_2.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
struct GameData {
// Live game data
String homeTeam;
String awayTeam;
int homeScore;
int awayScore;
String quarter;
String timeRemaining;
String gameStatus;
bool isGameLive;
// Next game data
String nextOpponent;
String nextGameDay;
String nextGameMonth;
String nextGameTime;
bool isHome;
// Season stats
int wins;
int losses;
int byeWeek;
float pointsPerGame;
float yardsPerGame;
float passYardsPerGame;
float rushYardsPerGame;
float pointsAllowedPerGame;
float yardsAllowedPerGame;
float passYardsAllowedPerGame;
float rushYardsAllowedPerGame;
bool isBroncosGame;
};
GameData currentGame;
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 30000; // 30 seconds
const char* ssid = "wifi";
const char* password = "pswd";
const char* scheduleUrl = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams/den/schedule";
const char* statsUrl = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams/den/statistics";
TFT_eSPI tft = TFT_eSPI();
#define BTTN_PIN 13
int displayMode = 0;
int lastState = HIGH;
int currentState;
void connectWiFi() {
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi Connected!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
pinMode(BTTN_PIN, INPUT_PULLUP);
tft.init();
tft.setRotation(1);
tft.setSwapBytes(true);
tft.fillScreen(TFT_BLACK);
connectWiFi();
}
void loop() {
currentState = digitalRead(BTTN_PIN);
if (currentState == LOW && lastState == HIGH) {
displayMode = (displayMode + 1) % 3;
if (displayMode == 1) {
// Mode 1: General stats and next game
tft.pushImage(0, 0, 320, 240, mode_1);
Serial.println("Mode 1: General Stats");
fetchGeneralData();
//debugPrintRawJson();
//displayGeneralMode();
} else if (displayMode == 2) {
// Mode 2: Live game
tft.pushImage(0, 0, 320, 240, mode_2);
Serial.println("Mode 2: Live Game");
//fetchLiveGameData();
displayLiveMode();
} else {
tft.fillScreen(TFT_BLACK);
Serial.println("Mode 0: Off");
}
Serial.println("button");
}
lastState = currentState;
}
void fetchGeneralData() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected");
return;
}
Serial.println("\n========== FETCHING DATA ==========");
HTTPClient http;
http.begin(scheduleUrl);
http.setTimeout(20000);
int httpCode = http.GET();
Serial.printf("HTTP Response Code: %d\n", httpCode);
if (httpCode != 200) {
Serial.println("❌ HTTP error");
http.end();
return;
}
// --- 1) Build FILTER document (from ArduinoJson Assistant) ---
// IMPORTANT: give it a capacity (e.g. 2048), NOT JsonDocument filter;
JsonDocument filter;
filter["team"]["recordSummary"] = true;
JsonObject filter_events_0 = filter["events"].add();
filter_events_0["name"] = true;
filter_events_0["shortName"] = true;
filter_events_0["date"] = true;
filter_events_0["seasonType"]["abbreviation"] = true;
filter_events_0["week"]["number"] = true;
JsonObject filter_events_0_competitions_0 = filter_events_0["competitions"].add();
filter_events_0_competitions_0["date"] = true;
filter_events_0_competitions_0["venue"]["address"]["city"] = true;
JsonObject filter_events_0_competitions_0_competitors_0 = filter_events_0_competitions_0["competitors"].add();
filter_events_0_competitions_0_competitors_0["order"] = true;
filter_events_0_competitions_0_competitors_0["team"]["displayName"] = true;
filter_events_0_competitions_0_competitors_0["score"]["value"] = true;
filter_events_0_competitions_0_competitors_0["record"][0]["displayValue"] = true;
JsonObject filter_events_0_competitions_0_status = filter_events_0_competitions_0["status"].to();
filter_events_0_competitions_0_status["clock"] = true;
filter_events_0_competitions_0_status["displayClock"] = true;
filter_events_0_competitions_0_status["period"] = true;
filter_events_0_competitions_0_status["type"]["state"] = true;
filter["byeWeek"] = true;
// (Optional) debug: show what the filter looks like
Serial.println("Filter doc:");
serializeJsonPretty(filter, Serial);
Serial.println();
// --- 2) Deserialize with filter from the HTTP stream ---
WiFiClient &input = http.getStream();
JsonDocument doc; // result after filtering (much smaller than raw JSON)
DeserializationError error = deserializeJson(
doc, input,
DeserializationOption::Filter(filter),
DeserializationOption::NestingLimit(30)); // bump nesting limit a bit
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
http.end();
return;
}
// Debug: check memory usage + preview
Serial.print("overflowed: ");
Serial.println(doc.overflowed());
Serial.print("memoryUsage: ");
Serial.println(doc.memoryUsage());
Serial.println("Filtered doc preview:");
serializeJsonPretty(doc, Serial);
Serial.println();
Serial.println("✓ JSON parsed successfully!");
// --- 3) team.recordSummary -> wins/losses ---
const char* team_recordSummary = doc["team"]["recordSummary"]; // "9-2"
if (team_recordSummary) {
int w = 0, l = 0;
if (sscanf(team_recordSummary, "%d-%d", &w, &l) == 2) {
currentGame.wins = w;
currentGame.losses = l;
Serial.printf("✓ Parsed record: %d-%d\n", w, l);
} else {
Serial.printf("⚠ Could not parse recordSummary: %s\n", team_recordSummary);
}
} else {
Serial.println("⚠ recordSummary missing");
}
// --- 4) Access events[] (you can later add your next-game logic here) ---
JsonArray events = doc["events"].as();
if (events.isNull() || events.size() == 0) {
Serial.println("⚠ No events in schedule");
http.end();
return;
}
Serial.printf("Events in filtered doc: %u\n", events.size());
// Example: print basic info for first event
JsonObject e0 = events[0];
const char* e0_date = e0["date"] | "";
const char* e0_name = e0["name"] | "";
const char* e0_shortName = e0["shortName"] | "";
int e0_week = e0["week"]["number"] | 0;
Serial.printf("Week %d: %s (%s) on %s\n",
e0_week, e0_name, e0_shortName, e0_date);
JsonObject comp0 = e0["competitions"][0];
const char* city = comp0["venue"]["address"]["city"] | "";
Serial.printf(" Venue city: %s\n", city);
JsonArray competitors = comp0["competitors"].as();
for (JsonObject c : competitors) {
int order = c["order"] | -1;
const char* teamName = c["team"]["displayName"] | "";
int score = c["score"]["value"] | -1;
Serial.printf(" [%d] %s score=%d\n", order, teamName, score);
}
int byeWeek = doc["byeWeek"] | -1;
if (byeWeek > 0) {
currentGame.byeWeek = byeWeek;
Serial.printf("Bye week: %d\n", byeWeek);
}
http.end();
}
void displayGeneralMode() {
Serial.println("\n========== DISPLAY: GENERAL MODE ==========");
Serial.printf("Record: %d-%d\n", currentGame.wins, currentGame.losses);
Serial.printf("Next Opponent: %s (%s)\n",
currentGame.nextOpponent.c_str(),
currentGame.isHome ? "HOME" : "AWAY");
Serial.printf("Points Per Game: %.1f\n", currentGame.pointsPerGame);
Serial.printf("Yards Per Game: %.1f\n", currentGame.yardsPerGame);
Serial.printf("Pass Yards Per Game: %.1f\n", currentGame.passYardsPerGame);
Serial.printf("Rush Yards Per Game: %.1f\n", currentGame.rushYardsPerGame);
Serial.println("===========================================\n");
}
void displayLiveMode() {
Serial.println("\n========== DISPLAY: LIVE GAME MODE ==========");
if (currentGame.isGameLive) {
Serial.printf("%s vs %s\n", currentGame.homeTeam.c_str(), currentGame.awayTeam.c_str());
Serial.printf("Score: %d - %d\n", currentGame.homeScore, currentGame.awayScore);
Serial.printf("Quarter: %s\n", currentGame.quarter.c_str());
Serial.printf("Time Remaining: %s\n", currentGame.timeRemaining.c_str());
} else {
Serial.println("No live game currently");
}
Serial.println("=============================================\n");
}
/*
// Display functions - you'll implement these based on your UI
void displayGeneralMode() {
// Draw the general stats on screen
tft.setTextColor(TFT_WHITE, TFT_NAVY);
tft.setTextSize(2);
// Example:
tft.setCursor(10, 10);
tft.printf("Record: %d-%d", currentGame.wins, currentGame.losses);
tft.setCursor(10, 40);
tft.printf("Next: vs %s", currentGame.nextOpponent.c_str());
tft.setCursor(10, 70);
tft.printf("PPG: %.1f", currentGame.pointsPerGame);
// Add more display code here
}
void displayLiveMode() {
// Draw the live game info on screen
if (currentGame.isGameLive) {
tft.setTextColor(TFT_WHITE, TFT_NAVY);
tft.setTextSize(3);
tft.setCursor(50, 80);
tft.printf("%d - %d", currentGame.homeScore, currentGame.awayScore);
tft.setTextSize(2);
tft.setCursor(80, 120);
tft.printf("Q%s %s", currentGame.quarter.c_str(), currentGame.timeRemaining.c_str());
} else {
tft.setTextSize(2);
tft.setCursor(60, 100);
tft.print("No live game");
}
}*/