diff --git a/src/F1_api.h b/src/F1_api.h index bbf0497..b6b2ac4 100644 --- a/src/F1_api.h +++ b/src/F1_api.h @@ -6,7 +6,7 @@ #include "ArduinoJson.h" #include "config.h" -struct race_info_t { +struct session_info_t { char session_name[50]; char date_start[32]; char circuit_short_name[50]; @@ -15,11 +15,69 @@ struct race_info_t { uint16_t circuit_key; }; -extern race_info_t race_data; +struct meeting_info_t { + uint16_t circuit_key; + uint16_t meeting_key; + char circuit_image_URL[160]; + char country_name[40]; + char location[40]; + char meeting_name[80]; +}; + +struct data_struct_t { + struct session_info_t session_info; + struct meeting_info_t meeting_info; +}; + +extern data_struct_t race_data; extern SemaphoreHandle_t dataMutex; -static int get_race_query_url(char* target, size_t target_size) { - if (target == NULL) return -1; +#include + +// Modifies the character array directly in-place +void cleanToASCIIInPlace(char* str) { + if (!str) return; + + size_t readIdx = 0; + size_t writeIdx = 0; + + // Process until the null-terminator + while (str[readIdx] != '\0') { + unsigned char c = str[readIdx]; + + // Handle common UTF-8 multi-byte sequences for Latin characters + if (c == 0xC3) { + readIdx++; // Move past the prefix byte (0xC3) + unsigned char next = str[readIdx]; + + switch(next) { + case 0xA3: str[writeIdx++] = 'a'; break; // ã -> a + case 0xA1: str[writeIdx++] = 'a'; break; // á -> a + case 0xA9: str[writeIdx++] = 'e'; break; // é -> e + case 0xAD: str[writeIdx++] = 'i'; break; // í -> i + case 0xB3: str[writeIdx++] = 'o'; break; // ó -> o + case 0xBA: str[writeIdx++] = 'u'; break; // ú -> u + case 0xA7: str[writeIdx++] = 'c'; break; // ç -> c + default: str[writeIdx++] = '?'; break; // Fallback for unhandled symbols + } + readIdx++; // Move past the second UTF-8 byte + } + // Standard printable ASCII filter + else { + if (c < 128) { + str[writeIdx++] = c; + } + readIdx++; + } + } + + // Explicitly add the new null-terminator to seal the shorter string + str[writeIdx] = '\0'; +} + + +static int get_future_meetings_url(char* target, size_t target_size) { + if (target == NULL) return -1; time_t now = time(nullptr); struct tm timeinfo, futureTime; @@ -32,24 +90,33 @@ static int get_race_query_url(char* target, size_t target_size) { char date_buffer[12]; char fdate_buffer[12]; - Serial.print(""); + strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", &timeinfo); - Serial.printf("Current Date: %s\n", date_buffer); strftime(fdate_buffer, sizeof(fdate_buffer), "%Y-%m-%d", &futureTime); - Serial.printf("Future Date: %s\n", fdate_buffer); - snprintf(target, target_size, "https://api.openf1.org/v1/sessions?date_start>=%s&date_start<=%s&session_type=Race", + snprintf(target, target_size, "https://api.openf1.org/v1/meetings?date_end>=%s&date_end<=%s", date_buffer, fdate_buffer); + Serial.print(""); + Serial.println(target); + + return 0; +} + +static int get_future_sessions_url(char* target, size_t target_size, SemaphoreHandle_t* mutex, uint16_t meeting) { + + snprintf(target, target_size, "https://api.openf1.org/v1/sessions?meeting_key=%d&session_type=Race", + meeting); + return 0; } -int parse_session_json(JsonArray sessions, race_info_t session) { +int parse_session_json(JsonArray sessions, session_info_t* target, const SemaphoreHandle_t* mutex) { JsonObject nextRaceJson = sessions[0]; - race_info_t localData; + session_info_t localData; const char* sessionName = nextRaceJson[F("session_name")]; const char* dateStart = nextRaceJson[F("date_start")]; @@ -66,17 +133,64 @@ int parse_session_json(JsonArray sessions, race_info_t session) { if (sessionName != NULL) { strlcpy(localData.session_name, sessionName, sizeof(localData.session_name)); + cleanToASCIIInPlace(localData.session_name); } else return -1; if (dateStart != NULL) { strlcpy(localData.date_start, dateStart, sizeof(localData.date_start)); + cleanToASCIIInPlace(localData.date_start); } else return -1; if (circuitName != NULL) { strlcpy(localData.circuit_short_name, circuitName, sizeof(localData.circuit_short_name)); + cleanToASCIIInPlace(localData.circuit_short_name); } else return -1; - if (xSemaphoreTake(dataMutex, MUTEX_TIMEOUT_TICKS)) { - race_data = localData; - xSemaphoreGive(dataMutex); + if (xSemaphoreTake(*mutex, MUTEX_TIMEOUT_TICKS)) { + *target = localData; + xSemaphoreGive(*mutex); + } else { + Serial.print(""); + Serial.println("Unable to acquire mutex, API loop."); + return -1; + } + + return 0; +} + +int parse_meeting_json(JsonArray sessions, meeting_info_t* target, const SemaphoreHandle_t* mutex) { + JsonObject nextRaceJson = sessions[0]; + meeting_info_t localData; + + const char* circuit_image_URL = nextRaceJson[F("circuit_image")]; + const char* country_name = nextRaceJson[F("country_name")]; + const char* location = nextRaceJson[F("location")]; + const char* meeting_name = nextRaceJson[F("meeting_name")]; + + localData.circuit_key = nextRaceJson[F("circuit_key")]; + if (localData.circuit_key == 0) return -1; + + localData.meeting_key = nextRaceJson[F("meeting_key")]; + if (localData.meeting_key == 0) return -1; + + if (circuit_image_URL != NULL) { + strlcpy(localData.circuit_image_URL, circuit_image_URL, sizeof(localData.circuit_image_URL)); + cleanToASCIIInPlace(localData.circuit_image_URL); + } else return -1; + if (country_name != NULL) { + strlcpy(localData.country_name, country_name, sizeof(localData.country_name)); + cleanToASCIIInPlace(localData.country_name); + } else return -1; + if (location != NULL) { + strlcpy(localData.location, location, sizeof(localData.location)); + cleanToASCIIInPlace(localData.location); + } else return -1; + if (meeting_name != NULL) { + strlcpy(localData.meeting_name, meeting_name, sizeof(localData.meeting_name)); + cleanToASCIIInPlace(localData.meeting_name); + } else return -1; + + if (xSemaphoreTake(*mutex, MUTEX_TIMEOUT_TICKS)) { + *target = localData; + xSemaphoreGive(*mutex); } else { Serial.print(""); Serial.println("Unable to acquire mutex, API loop."); @@ -98,7 +212,7 @@ void get_next_race(void *pvParameters) { http.setCACert(OPENF1_ROOT_CA); char query_url[103]; - get_race_query_url(query_url, sizeof(query_url)); + get_future_meetings_url(query_url, sizeof(query_url)); const bool requestReady = http.begin(query_url); if (!requestReady) { @@ -113,9 +227,9 @@ void get_next_race(void *pvParameters) { JsonDocument json; const DeserializationError err = deserializeJson(json, responseStream); if (err == DeserializationError::Ok) { - JsonArray sessions = json.as(); - if (sessions.size() != 0) { - parse_session_json(sessions, race_data); + JsonArray meetings = json.as(); + if (meetings.size() != 0) { + parse_meeting_json(meetings, &race_data.meeting_info, &dataMutex); } } else { Serial.print(""); @@ -134,6 +248,51 @@ void get_next_race(void *pvParameters) { } } http.end(); + + HTTPClient http2; + http2.setTimeout(10000); + http2.setCACert(OPENF1_ROOT_CA); + + get_future_sessions_url(query_url, sizeof(query_url), &dataMutex, race_data.meeting_info.meeting_key); + + Serial.print(""); + Serial.println(query_url); + + const bool requestReady2 = http2.begin(query_url); + if (!requestReady) { + Serial.print(""); + Serial.println("Failed to initialize OpenF1 request."); + } else { + const int httpResponseCode = http2.GET(); + if (httpResponseCode > 0) { + Stream& responseStream = http2.getStream(); + Serial.printf("HTTP Size: %d\n", http2.getSize()); + + JsonDocument json; + const DeserializationError err = deserializeJson(json, responseStream); + if (err == DeserializationError::Ok) { + JsonArray sessions = json.as(); + if (sessions.size() != 0) { + parse_session_json(sessions, &race_data.session_info, &dataMutex); + } + } else { + Serial.print(""); + Serial.print("Failed to parse JSON response: "); + Serial.println(err.c_str()); + } + + retryDelay = pdMS_TO_TICKS(900000); + } else { + Serial.print(""); + Serial.printf( + "Error on GET: %d (%s)\r\n", + httpResponseCode, + HTTPClient::errorToString(httpResponseCode).c_str()); + retryDelay = pdMS_TO_TICKS(10000); + } + } + http.end(); + } else { Serial.print(""); Serial.println("WiFi not connecteFormattedd."); diff --git a/src/main.cpp b/src/main.cpp index 4faec28..9485088 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,13 +17,13 @@ extern "C" { } static UBYTE displayBuffer[(EPD_4IN2_V2_WIDTH / 8) * EPD_4IN2_V2_HEIGHT]; -static constexpr UWORD COUNTER_WIDTH = 200; -static constexpr UWORD COUNTER_HEIGHT = 24; +static constexpr UWORD COUNTER_WIDTH = 400; +static constexpr UWORD COUNTER_HEIGHT = 38; static constexpr UWORD COUNTER_Y = 20; static UBYTE counterBuffer[(COUNTER_WIDTH / 8) * COUNTER_HEIGHT]; static int counter = 0; -race_info_t race_data; +data_struct_t race_data; SemaphoreHandle_t dataMutex; void wifi_connect() { @@ -81,7 +81,7 @@ bool readLocalTime(struct tm* result) { } void display_loop(void *pvParameters) { - static race_info_t local_data; + static data_struct_t local_data; while (true) { static unsigned long lastRefresh = 0; static char counterStr[32]; @@ -105,16 +105,24 @@ void display_loop(void *pvParameters) { } - if (race_data.session_key == 0) { + if (local_data.meeting_info.meeting_key == 0) { snprintf(counterStr, sizeof(counterStr), "Counter: %d", counter); Serial.print(""); Serial.println(counterStr); } else { - snprintf(counterStr, sizeof(counterStr), "%s", race_data.circuit_short_name); + snprintf(counterStr, sizeof(counterStr), "%s", local_data.meeting_info.meeting_name); } Paint_Clear(WHITE); - Paint_DrawString_EN(10, 0, counterStr, &Font16, WHITE, BLACK); + + if (local_data.session_info.date_start[0] != 0) { + Serial.print(""); + Serial.printf("date start: %s\n", local_data.session_info.date_start); + Paint_DrawString_EN(10, 22, local_data.session_info.date_start, &Font16, WHITE, BLACK); + } + + + Paint_DrawString_EN(10, 0, counterStr, &Font16, WHITE, BLACK); EPD_4IN2_V2_PartialDisplay( counterBuffer, @@ -149,14 +157,13 @@ void setup() { Paint_SelectImage(displayBuffer); Paint_Clear(WHITE); Paint_DrawString_EN(10, 0, "F1 Display Booting...", &Font16, WHITE, BLACK); - Paint_DrawString_EN(10, COUNTER_Y, "Counter: 0", &Font16, WHITE, BLACK); EPD_4IN2_V2_Display(displayBuffer); DEV_Delay_ms(500); wifi_connect(); sync_time(); - printf("Begin display loop.\n"); + Serial.println("Begin display loop.\n"); Paint_NewImage(counterBuffer, COUNTER_WIDTH, COUNTER_HEIGHT, ROTATE_0, WHITE); Paint_SelectImage(counterBuffer);