Compare commits
6
Commits
42e51a4a1c
...
pico_dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f429f32890 | ||
|
|
b67432ed5d | ||
|
|
c0e71374eb | ||
|
|
3ec87ada9c | ||
|
|
f12c812d73 | ||
|
|
5e6606e6cc |
No files matched your search
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cmake.ignoreCMakeListsMissing": true
|
||||
}
|
||||
@@ -12,3 +12,5 @@
|
||||
// Display resolution
|
||||
#define EPD_4IN2B_V2_WIDTH 400
|
||||
#define EPD_4IN2B_V2_HEIGHT 300
|
||||
|
||||
#define MUTEX_TIMEOUT_TICKS pdMS_TO_TICKS(150)
|
||||
+3
-2
@@ -14,6 +14,7 @@ board = rpipicow
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
build_flags =
|
||||
-I include
|
||||
-DPIO_FRAMEWORK_ARDUINO_ENABLE_FREERTOS
|
||||
-I include
|
||||
-DPIO_FRAMEWORK_ARDUINO_ENABLE_FREERTOS
|
||||
build_src_filter = +<main.cpp>
|
||||
lib_deps = bblanchon/ArduinoJson@7.4.3
|
||||
+311
-7
@@ -3,26 +3,282 @@
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#include "secrets.h"
|
||||
#include "ArduinoJson.h"
|
||||
#include "config.h"
|
||||
#include "time.h"
|
||||
|
||||
extern "C" char *strptime(const char *__restrict, const char *__restrict, struct tm *__restrict); // Not exposed by headers?
|
||||
|
||||
// Portable replacement for missing timegm()
|
||||
time_t timegm(struct tm *t) {
|
||||
int year = t->tm_year + 1900;
|
||||
int mon = t->tm_mon + 1; // 1 to 12
|
||||
int day = t->tm_mday;
|
||||
|
||||
// Shift year/month to make March month 0 (simplifies leap year calculation)
|
||||
int a = (14 - mon) / 12;
|
||||
int y = year + 4800 - a;
|
||||
int m = mon + 12 * a - 3;
|
||||
|
||||
// Calculate Julian Day Number
|
||||
long jdn = day + (153 * m + 2) / 5 + 365L * y + y / 4 - y / 100 + y / 400 - 32045;
|
||||
|
||||
// Days since Unix epoch (1970-01-01 is JDN 2440588)
|
||||
long days = jdn - 2440588;
|
||||
|
||||
return (time_t)(days * 86400L +
|
||||
t->tm_hour * 3600L +
|
||||
t->tm_min * 60L +
|
||||
t->tm_sec);
|
||||
}
|
||||
|
||||
struct session_info_t {
|
||||
char session_name[50];
|
||||
struct tm date_start;
|
||||
char circuit_short_name[50];
|
||||
uint16_t session_key;
|
||||
uint16_t meeting_key;
|
||||
uint16_t circuit_key;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// 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;
|
||||
|
||||
gmtime_r(&now, &timeinfo);
|
||||
|
||||
futureTime = timeinfo;
|
||||
futureTime.tm_mon += 2;
|
||||
mktime(&futureTime);
|
||||
|
||||
char date_buffer[28];
|
||||
char fdate_buffer[28];
|
||||
|
||||
strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%dT%H:%M:%S+00:00", &timeinfo);
|
||||
|
||||
strftime(fdate_buffer, sizeof(fdate_buffer), "%Y-%m-%dT%H:%M:%S+00:00", &futureTime);
|
||||
|
||||
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, session_info_t* target, const SemaphoreHandle_t* mutex) {
|
||||
JsonObject nextRaceJson = sessions[0];
|
||||
session_info_t localData;
|
||||
|
||||
struct tm utc_tm;
|
||||
memset(&utc_tm, 0, sizeof(struct tm));
|
||||
strptime(nextRaceJson[F("date_start")], "%Y-%m-%dT%H:%M:%S", &utc_tm);
|
||||
|
||||
// 2. Convert UTC struct tm to time_t Unix timestamp
|
||||
time_t raw_time = timegm(&utc_tm);
|
||||
|
||||
// 3. Convert time_t to local struct tm
|
||||
struct tm local_tm;
|
||||
localtime_r(&raw_time, &localData.date_start);
|
||||
|
||||
char buffer[64];
|
||||
strftime(buffer, sizeof(buffer), "%Y-%m-%d %I:%M:%S %p %Z", &local_tm);
|
||||
Serial.print("");
|
||||
Serial.print("Session local: ");
|
||||
Serial.println(buffer);
|
||||
|
||||
const char* sessionName = nextRaceJson[F("session_name")];
|
||||
const char* circuitName = nextRaceJson[F("circuit_short_name")];
|
||||
|
||||
localData.session_key = nextRaceJson[F("session_key")];
|
||||
if (localData.session_key == 0) return -1;
|
||||
|
||||
localData.meeting_key = nextRaceJson[F("meeting_key")];
|
||||
if (localData.meeting_key == 0) return -1;
|
||||
|
||||
localData.circuit_key = nextRaceJson[F("circuit_key")];
|
||||
if (localData.circuit_key == 0) return -1;
|
||||
if (localData.date_start.tm_year == 0) return -1;
|
||||
|
||||
if (sessionName != NULL) {
|
||||
strlcpy(localData.session_name, sessionName, sizeof(localData.session_name));
|
||||
cleanToASCIIInPlace(localData.session_name);
|
||||
} 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(*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.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void get_next_race(void *pvParameters) {
|
||||
TickType_t retryDelay = pdMS_TO_TICKS(10000);
|
||||
while (true) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
|
||||
retryDelay = pdMS_TO_TICKS(10000);
|
||||
|
||||
HTTPClient http;
|
||||
http.setTimeout(10000);
|
||||
http.setCACert(OPENF1_ROOT_CA);
|
||||
const bool requestReady = http.begin(
|
||||
"https://api.openf1.org/v1/sessions?date_start%3E=2026-11-01&date_start%3C=2026-12-01&session_type=Race");
|
||||
|
||||
char query_url[150];
|
||||
get_future_meetings_url(query_url, sizeof(query_url));
|
||||
|
||||
const bool requestReady = http.begin(query_url);
|
||||
if (!requestReady) {
|
||||
Serial.print("");
|
||||
Serial.println("Failed to initialize OpenF1 request.");
|
||||
} else {
|
||||
const int httpResponseCode = http.GET();
|
||||
if (httpResponseCode > 0) {
|
||||
String response = http.getString();
|
||||
Serial.print("");
|
||||
Serial.println("Received response from OpenF1 API:");
|
||||
Serial.println(response);
|
||||
Stream& responseStream = http.getStream();
|
||||
Serial.printf("HTTP Size: %d\n", http.getSize());
|
||||
|
||||
JsonDocument json;
|
||||
const DeserializationError err = deserializeJson(json, responseStream);
|
||||
if (err == DeserializationError::Ok) {
|
||||
JsonArray meetings = json.as<JsonArray>();
|
||||
if (meetings.size() != 0) {
|
||||
if (parse_meeting_json(meetings, &race_data.meeting_info, &dataMutex) != 0) {
|
||||
Serial.print("");
|
||||
Serial.println(F("Failed to parse meeting data."));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.print("");
|
||||
Serial.print(F("Failed to parse JSON response: "));
|
||||
Serial.println(err.c_str());
|
||||
}
|
||||
|
||||
retryDelay = pdMS_TO_TICKS(900000);
|
||||
} else {
|
||||
Serial.print("");
|
||||
@@ -34,9 +290,57 @@ 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<JsonArray>();
|
||||
if (sessions.size() != 0) {
|
||||
if (parse_session_json(sessions, &race_data.session_info, &dataMutex) != 0) {
|
||||
Serial.print("");
|
||||
Serial.println(F("Failed to parse session data"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.print("");
|
||||
Serial.print(F("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 connected.");
|
||||
Serial.println(F("WiFi not connected."));
|
||||
}
|
||||
vTaskDelay(retryDelay);
|
||||
}
|
||||
|
||||
+50
-18
@@ -5,6 +5,7 @@
|
||||
#include "F1_api.h"
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#include <semphr.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -16,12 +17,15 @@ 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;
|
||||
|
||||
data_struct_t race_data;
|
||||
SemaphoreHandle_t dataMutex;
|
||||
|
||||
void wifi_connect() {
|
||||
WiFi.setHostname(DEVICE_HOSTNAME);
|
||||
Serial.println("Connecting to WiFi...");
|
||||
@@ -52,7 +56,7 @@ void wifi_connect() {
|
||||
|
||||
void sync_time() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("Skipping NTP synchronization because WiFi is disconnected.");
|
||||
Serial.println(F("Skipping NTP synchronization because WiFi is disconnected."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,7 +71,7 @@ void sync_time() {
|
||||
Serial.print("Time synchronized: ");
|
||||
Serial.println(asctime(&timeinfo));
|
||||
} else {
|
||||
Serial.println("Failed to synchronize time within 10 seconds.");
|
||||
Serial.println(F("Failed to synchronize time within 10 seconds."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +81,7 @@ bool readLocalTime(struct tm* result) {
|
||||
}
|
||||
|
||||
void display_loop(void *pvParameters) {
|
||||
static data_struct_t local_data;
|
||||
while (true) {
|
||||
static unsigned long lastRefresh = 0;
|
||||
static char counterStr[32];
|
||||
@@ -89,12 +94,34 @@ void display_loop(void *pvParameters) {
|
||||
lastRefresh = millis();
|
||||
|
||||
counter++;
|
||||
snprintf(counterStr, sizeof(counterStr), "Counter: %d", counter);
|
||||
Serial.print("");
|
||||
Serial.println(counterStr);
|
||||
|
||||
if (xSemaphoreTake(dataMutex, MUTEX_TIMEOUT_TICKS)) {
|
||||
local_data = race_data;
|
||||
xSemaphoreGive(dataMutex);
|
||||
//Display will continue to use old data
|
||||
} else {
|
||||
Serial.print("");
|
||||
Serial.println("Failed to acquire mutex in display thread!");
|
||||
}
|
||||
|
||||
|
||||
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", local_data.meeting_info.meeting_name);
|
||||
}
|
||||
|
||||
Paint_Clear(WHITE);
|
||||
Paint_DrawString_EN(10, 0, counterStr, &Font16, BLACK, WHITE);
|
||||
|
||||
if (local_data.session_info.date_start.tm_year != 0) {
|
||||
char date_buffer[30];
|
||||
strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d %I:%M:%S %p %Z", &local_data.session_info.date_start);
|
||||
Paint_DrawString_EN(10, 22, date_buffer, &Font16, WHITE, BLACK);
|
||||
}
|
||||
|
||||
Paint_DrawString_EN(10, 0, counterStr, &Font16, WHITE, BLACK);
|
||||
|
||||
EPD_4IN2_V2_PartialDisplay(
|
||||
counterBuffer,
|
||||
@@ -110,6 +137,12 @@ void display_loop(void *pvParameters) {
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
dataMutex = xSemaphoreCreateMutex();
|
||||
if (dataMutex == NULL) {
|
||||
Serial.println("Failed to create mutex!");
|
||||
while(1);
|
||||
}
|
||||
|
||||
if (DEV_Module_Init() != 0) {
|
||||
Serial.println("Failed to initialize display module.");
|
||||
return;
|
||||
@@ -117,24 +150,23 @@ void setup() {
|
||||
|
||||
EPD_4IN2_V2_Init();
|
||||
Serial.println("e-Paper initialization complete; clearing display...");
|
||||
EPD_4IN2_V2_Clear();
|
||||
//EPD_4IN2_V2_Clear();
|
||||
|
||||
Paint_NewImage(displayBuffer, EPD_4IN2_V2_WIDTH, EPD_4IN2_V2_HEIGHT, ROTATE_0, WHITE);
|
||||
Paint_SelectImage(displayBuffer);
|
||||
Paint_Clear(WHITE);
|
||||
Paint_DrawString_EN(10, 0, "F1 Display Booting...", &Font16, WHITE, BLACK);
|
||||
EPD_4IN2_V2_Display(displayBuffer);
|
||||
|
||||
DEV_Delay_ms(500);
|
||||
|
||||
wifi_connect();
|
||||
sync_time();
|
||||
printf("Begin display loop.\n");
|
||||
|
||||
Paint_NewImage(displayBuffer, EPD_4IN2_V2_WIDTH, EPD_4IN2_V2_HEIGHT, ROTATE_0, WHITE);
|
||||
Paint_SelectImage(displayBuffer);
|
||||
Paint_Clear(WHITE);
|
||||
Paint_DrawString_EN(10, 0, "F1 Display Test", &Font16, BLACK, WHITE);
|
||||
Paint_DrawString_EN(10, COUNTER_Y, "Counter: 0", &Font16, BLACK, WHITE);
|
||||
EPD_4IN2_V2_Display(displayBuffer);
|
||||
Serial.println("Begin display loop.\n");
|
||||
|
||||
Paint_NewImage(counterBuffer, COUNTER_WIDTH, COUNTER_HEIGHT, ROTATE_0, WHITE);
|
||||
Paint_SelectImage(counterBuffer);
|
||||
|
||||
xTaskCreate(display_loop, "display_loop", 1024, NULL, 5, NULL);
|
||||
xTaskCreate(get_next_race, "get_next_race", 4096, NULL, 5, NULL);
|
||||
xTaskCreate(get_next_race, "get_next_race", 2048, NULL, 5, NULL);
|
||||
}
|
||||
Reference in new issue
Block a user