responseBody
returns a String
; if there's a Content-Length
in the headers, it will reserve
that amount of space. That way, the String
won't have to repeatedly grow, which can cause heap fragmentation. Unfortunately, perusing the headers
$ curl -i --http1.1 https://api-web.nhle.com/v1/scoreboard/CHI/now
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
it's chunked
instead. The total size right now is about 19KB. I used ArduinoHttpClient on ESP32 (instead of the built-in one), where there is enough memory, and had it report when the buffer was relocated after adding another char
: length and hex address
0
3FFB222C
15
3FFD6904
16
3FFD759C
224
3FFD5204
1040
3FFD6068
1520
3FFD797C
1920
3FFDA6E0
1968
3FFDB7B4
7232
3FFDDD18
8480
3FFD94B8
10032
3FFE05EC
14624
3FFD797C
15344
3FFE46C4
body length: 19004
The buffer moves around a bit. So you need even more memory than the payload itself. The Nano 33 IoT has 32KB of RAM?
I also fiddled with the payload, to get an idea of what you'd need to get the current score. This is JavaScript, which you can run in the browser's developer Console (after having the browser make the API call so you're on the same domain). Do it when there is a live game.
// get JSON payload
j = await fetch('https://api-web.nhle.com/v1/scoreboard/CHI/now').then(r => r.json());
// find LIVE game
g = j.gamesByDate.flatMap(g => g.games).find(g => g.gameState == 'LIVE');
// extract scores
s = g && [g.awayTeam, g.homeTeam].map(({abbrev, score}) => ({team: abbrev, score}))
// team score
t = s?.find(t => t.team == 'CHI').score
Even if you retrieve parts of the body, you'd need to find the "LIVE"
game -- if I understand what you're trying to do with it. Then grab the score from either the "awayTeam"
or "homeTeam"
in the same nested JSON object, whichever is "CHI"
. It would be at a different location every (game) day.
You could actually do this with a state machine that understands at least a subset of JSON syntax, reading one byte at a time
class ScoreMachine {
String team;
unsigned long pos, bad;
String level;
unsigned gameLevel;
enum Place {
zero, // default, could start anything
name, // after { or following ,
str, // inside double-quotes
num, // in a number
} place = zero, last = zero;
bool escape = false;
String word;
String strVal;
double numVal;
void shift(Place p) {
last = place;
place = p;
}
void stat(bool k) {
if (!bad && !k) {
bad = pos;
}
}
char currentLevel() {
auto len = level.length();
if (len) {
return level[--len];
}
return '\0';
}
void pop(char c) {
auto len = level.length();
if (len) {
c = c == '}' ? '{' : '[';
if (c == level[--len]) {
level.remove(len);
return;
}
}
stat(false);
}
String "ed() {
return last == name ? word : strVal;
}
char backslash(char c) {
switch (c) {
case '\\': return '\\';
case 'n': return '\n';
case '"': return '"';
}
return c;
}
String gameState;
String abbrev;
int score = -1;
public:
int answer = -1;
ScoreMachine(String teamAbbrev) : team(teamAbbrev) {}
bool feed(char c) {
++pos;
if (bad) {
return true;
}
switch (place) {
case str:
if (escape) {
stat(quoted().concat(backslash(c)));
escape = false;
} else if (c == '"') {
place = zero;
if (word == "abbrev") {
abbrev = quoted();
} else if (word == "gameState") {
gameState = quoted();
gameLevel = level.length();
}
} else if (c == '\\') {
escape = true;
} else {
stat(quoted().concat(c));
}
return false;
case num:
if (c >= '0' && c <= '9') {
numVal *= 10;
numVal += c - '9';
return false;
} else if (c == '.') {
//TODO
return false;
}
shift(zero);
if (word == "score") {
score = numVal;
}
//fallthrough
}
switch (c) {
case ' ': case '\t': case '\n': case '\r':
return false;
case ':':
if (last != name) {
stat(false);
}
shift(zero);
return false;
case ',':
if (currentLevel() == '{') {
shift(name);
}
return false;
case '{': case '[':
stat(level.concat(c));
if (c == '{') {
shift(name);
}
return false;
case '}': case ']':
pop(c);
if (c == '}') {
if (answer < 0 && gameState == "LIVE" && abbrev == team && score >= 0) {
answer = score;
return true;
}
if (level.length() == gameLevel) {
score = -1;
}
}
return false;
case '"':
shift(str);
quoted().remove(0);
return false;
case '0'...'9':
numVal = c - '0';
shift(num);
return false;
}
return false;
}
unsigned long badPos() {
return bad;
}
} m("CHI"); // your team abbreviation here
void loop() {
Serial.println("making GET request");
client.get("/v1/scoreboard/CHI/now");
int statusCode = client.responseStatusCode();
Serial.print("Status code: ");
Serial.println(statusCode);
client.skipResponseHeaders();
if (client.isResponseChunked()) {
Serial.println("chunked!");
}
if (statusCode == 200) {
unsigned long len = 0;
do {
int c = client.read();
if (c >= 0) {
++len;
// if (len % 100 == 0) Serial.println(len);
if (m.feed(static_cast<char>(c))) {
break;
}
}
} while (client.available()); // read loop could be more robust
if (m.answer >= 0) {
Serial.print("at: ");
Serial.print(len);
Serial.print(" score: ");
Serial.println(m.answer);
} else if (m.badPos()) {
Serial.print("scan broken at pos: ");
Serial.println(m.badPos());
} else {
Serial.print("not found after: ");
Serial.println(len);
}
} else {
Serial.println(client.responseBody());
}
client.stop();
for (;;); // only try once
}
The extra code is a lot smaller than a typical payload. Worked at least once (with a saved payload)
making GET request
Status code: 200
chunked!
at: 11055 score: 3