#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <ArduinoJson.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <TimeLib.h>
#include <time.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define BUTTON_PIN 0
#define SSD1306_I2C_ADDRESS 0x3C
#define TESTING true // Set to true for testing with static JSON data
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* apiUrl = "https://api.balldontlie.io/v1/games?dates[]=";
const char* apiKey = "85cc8211-92f7-41e4-948a-006fb81f19cd";
// 取得網路時間相關參數設定
const char* ntpServer = "time.google.com"; // Google - NTP Server
const long gmtOffset_sec = -5*60*60; //美國東部標準時間-5hr
const int daylightOffset_sec = 0; //美國東部無日光節約時間
int currentGameIndex = 0;
JsonDocument jsonDoc;
String ETdate() {
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){
Serial.println("Failed to obtain time");
return "2025-06-19";
}
return String(timeinfo.tm_year + 1900) + "-" + String(timeinfo.tm_mon + 1) + "-" + String(timeinfo.tm_mday);
}
void displayGameInfo(int index) {
display.clearDisplay();
if (index >= jsonDoc["data"].size()) {
display.println("No games available");
display.display();
return;
}
JsonObject game = jsonDoc["data"][index];
String status = game["status"].as<String>();
String homeTeam = game["home_team"]["abbreviation"].as<String>();
String visitorTeam = game["visitor_team"]["abbreviation"].as<String>();
String time = game["time"].as<String>();
int period = game["period"].as<int>();
//---------------------------------------------------------------------
if(period == 0){
Serial.println("Game not started yet");
String startTime = status;
String formatted = "";
if (startTime.length() >= 16) {
String month = startTime.substring(5, 7);
String day = startTime.substring(8, 10);
String timePart = startTime.substring(11, 16);
formatted = timePart + " EST";
} else {
formatted = startTime;
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
int16_t x1, y1;
uint16_t w, h;
// 第一行:開始時間
display.setTextSize(2);
display.getTextBounds(formatted, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 0);
display.print(formatted);
// 第二行:A vs B
display.setTextSize(2);
String vs = String(homeTeam) + " vs " + String(visitorTeam);
display.getTextBounds(vs, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, h + 10);
display.print(vs);
display.display();
//---------------------------------------------------------------------
}else{
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
int16_t x1, y1;
uint16_t w, h, tb_w, tb_h, sb_w, sb_h;
// 第一行:節數-時間
display.setTextSize(2);
display.getTextBounds(time, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 0);
display.print(time);
// 第二行:隊名左右對齊
display.setTextSize(2);
int16_t y2 = h + 4;
display.setCursor(0, y2);
display.print(homeTeam);
display.getTextBounds(visitorTeam, 0, 0, &x1, &y1, &tb_w, &tb_h);
display.setCursor(SCREEN_WIDTH - tb_w, y2);
display.print(visitorTeam);
// 第三行:三位數比分左右對齊
display.setTextSize(3);
int16_t y3 = y2 + tb_h + 2;
char bufA[5], bufB[5];
sprintf(bufA, "%03d", game["home_team_score"].as<int>());
sprintf(bufB, "%03d", game["visitor_team_score"].as<int>());
display.setCursor(0, y3);
display.print(bufA);
display.getTextBounds(bufB, 0, 0, &x1, &y1, &sb_w, &sb_h);
display.setCursor(SCREEN_WIDTH - sb_w, y3);
display.print(bufB);
display.display();
}
display.display();
}
void fetchGamesData() {
if(TESTING){
// For testing purposes, you can use a static JSON string instead of fetching from the API
const char* FinalGame = "{\"data\":[{\"id\":18436949,\"date\":\"2025-05-15\",\"season\":2024,\"status\":\"Final\",\"period\":4,\"time\":\"Final\",\"postseason\":true,\"home_team_score\":119,\"visitor_team_score\":107,\"datetime\":\"2025-05-16T00:30:00.000Z\",\"home_team\":{\"id\":8,\"conference\":\"West\",\"division\":\"Northwest\",\"city\":\"Denver\",\"name\":\"Nuggets\",\"full_name\":\"DenverNuggets\",\"abbreviation\":\"DEN\"},\"visitor_team\":{\"id\":21,\"conference\":\"West\",\"division\":\"Northwest\",\"city\":\"OklahomaCity\",\"name\":\"Thunder\",\"full_name\":\"OklahomaCityThunder\",\"abbreviation\":\"OKC\"}}],\"meta\":{\"per_page\":25}}";
const char* EndQtr = "{\"data\":[{\"id\":18435678,\"date\":\"2025-05-16\",\"season\":2024,\"status\":\"3rdQtr\",\"period\":3,\"time\":\"ENDQ3\",\"postseason\":true,\"home_team_score\":92,\"visitor_team_score\":51,\"datetime\":\"2025-05-17T00:00:00.000Z\",\"home_team\":{\"id\":20,\"conference\":\"East\",\"division\":\"Atlantic\",\"city\":\"NewYork\",\"name\":\"Knicks\",\"full_name\":\"NewYorkKnicks\",\"abbreviation\":\"NYK\"},\"visitor_team\":{\"id\":2,\"conference\":\"East\",\"division\":\"Atlantic\",\"city\":\"Boston\",\"name\":\"Celtics\",\"full_name\":\"BostonCeltics\",\"abbreviation\":\"BOS\"}}],\"meta\":{\"per_page\":25}}";
const char* OngoingQtr = "{\"data\":[{\"id\":18435678,\"date\":\"2025-05-16\",\"season\":2024,\"status\":\"3rdQtr\",\"period\":3,\"time\":\"Q31:49\",\"postseason\":true,\"home_team_score\":82,\"visitor_team_score\":51,\"datetime\":\"2025-05-17T00:00:00.000Z\",\"home_team\":{\"id\":20,\"conference\":\"East\",\"division\":\"Atlantic\",\"city\":\"NewYork\",\"name\":\"Knicks\",\"full_name\":\"NewYorkKnicks\",\"abbreviation\":\"NYK\"},\"visitor_team\":{\"id\":2,\"conference\":\"East\",\"division\":\"Atlantic\",\"city\":\"Boston\",\"name\":\"Celtics\",\"full_name\":\"BostonCeltics\",\"abbreviation\":\"BOS\"}}],\"meta\":{\"per_page\":25}}";
const char* PreGame = "{\"data\":[{\"id\":18436952,\"date\":\"2025-05-18\",\"season\":2024,\"status\":\"2025-05-18T19:30:00Z\",\"period\":0,\"time\":null,\"postseason\":true,\"home_team_score\":0,\"visitor_team_score\":0,\"datetime\":\"2025-05-18T19:30:00.000Z\",\"home_team\":{\"id\":21,\"conference\":\"West\",\"division\":\"Northwest\",\"city\":\"OklahomaCity\",\"name\":\"Thunder\",\"full_name\":\"OklahomaCityThunder\",\"abbreviation\":\"OKC\"},\"visitor_team\":{\"id\":8,\"conference\":\"West\",\"division\":\"Northwest\",\"city\":\"Denver\",\"name\":\"Nuggets\",\"full_name\":\"DenverNuggets\",\"abbreviation\":\"DEN\"}}],\"meta\":{\"per_page\":25}}";
// Choose from these 4 static JSON strings for testing: FinalGame, EndQtr, OngoingQtr, PreGame
deserializeJson(jsonDoc, FinalGame);
Serial.println("Data fetched successfully");
Serial.println("Number of games: " + String(jsonDoc["data"].size()));
}else{
String url = String(apiUrl) + ETdate();
HTTPClient http;
http.begin(url);
http.addHeader("Authorization", apiKey);
Serial.println("Fetching data from: " + url);
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
deserializeJson(jsonDoc, payload);
Serial.println("Data fetched successfully");
Serial.println("Number of games: " + String(jsonDoc["data"].size()));
} else {
Serial.println("Error fetching data");
}
http.end();
}
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Initialize OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, SSD1306_I2C_ADDRESS)) {
Serial.println("SSD1306 allocation failed");
for (;;);
}
display.clearDisplay();
// Connect to WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
display.print("Connecting to WiFi...");
}
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
// DNS lookup for api.balldontlie.io
IPAddress testIP;
if (!WiFi.hostByName("api.balldontlie.io", testIP)) {
Serial.println("DNS lookup failed!");
} else {
Serial.print("api.balldontlie.io IP: ");
Serial.println(testIP);
}
// WiFi 連線成功後再設定 NTP
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("NTP server configured");
Serial.println("ET date: " + String(ETdate()));
fetchGamesData();
}
void loop() {
size_t numGames = jsonDoc["data"].size();
if (numGames == 0) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("No games found.");
Serial.println("No games found.");
display.display();
delay(5000);
return;
}
currentGameIndex = (currentGameIndex + 1) % numGames;
displayGameInfo(currentGameIndex);
Serial.println("Displaying game index: " + String(currentGameIndex));
Serial.println("Current game: " + jsonDoc["data"][currentGameIndex]["home_team"]["abbreviation"].as<String>() + " vs " + jsonDoc["data"][currentGameIndex]["visitor_team"]["abbreviation"].as<String>());
delay(5000); // 每5秒更新一次顯示
}