Community project
Live F1 Race Tracker
Andrew Wolfe
Published July 28, 2026 · Updated July 28, 2026

This project builds a live F1 race tracker that displays real-time telemetry and position data on an 800x480 display powered by an ESP32. The tracker fetches live race information from the OpenF1 API over WiFi, rendering car positions, gaps, and driver codes with an interactive touch interface.
The guide provides a complete parts list, wiring diagram, and step-by-step assembly instructions for housing the ESP32 and display in a 3D-printed enclosure. Builders will also receive the full firmware with WiFi configuration, API integration, and touch-screen controls to monitor their favorite drivers during live races.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| USB-C 5V Adapter5 V, 1 A minimum | 1 | USB-C wall adapter delivering regulated 5 V to the board's USB or VBUS rail. Default wired power source for desktop / stationary projects. |
Assembly
5 stepsPrint the two enclosure parts
Open data/waveshare_esp32_s3_touch_lcd_7_case.scad in OpenSCAD. Export one STL with PART set to "bezel" and a second with PART set to "back". Print both in PLA or PETG at 0.20 mm layers with at least three perimeters.
- Tip: Print the bezel front-face down and the back with its flat rear face on the bed.
- Tip: The source is parametric: adjust board_t or cable_x only after measuring your physical board and connector location.
- ⚠ Do not scale the models; scaling changes clearances and the M3 screw-hole size.
Test-fit the board and cable
Place the Waveshare display board into the rear tray with the display facing the open side. Feed the USB-C cable through the bottom notch and verify it reaches the board without sharply bending the cable.
- Tip: The enclosure intentionally grips the board perimeter, so it does not depend on unverified PCB mounting-hole locations.
- Tip: If the USB-C connector is not centered on your board orientation, change cable_x in the OpenSCAD file and re-export the back only.
- ⚠ Do not force the LCD or touch glass against the bezel; increase clearance or board_t if the board does not sit flat.
Close and fasten the enclosure
Place the bezel over the display, align its four outer corner ears with the back, and install four M3×10 screws. Tighten each screw gradually in a diagonal pattern until the two printed parts meet without bowing the screen.
- Tip: Use M3 heat-set inserts in the rear ears for a durable case, or drive the screws gently into the printed holes for a light-duty enclosure.
- ⚠ Do not overtighten the screws: excess force can flex the display assembly or crack printed plastic.
Power the dashboard
Connect the USB-C cable to the board and to a regulated 5 V USB supply or computer. The display board, touchscreen, and Wi-Fi radio are factory integrated; no LCD or touch wiring is required.
- Tip: Use a quality 5 V source rated for at least 1 A, especially with high LCD brightness.
- ⚠ Do not connect an unknown external supply at the same time as USB-C power.
Use the F1 dashboard
Deploy the existing firmware after entering the Wi-Fi details in its configured fields. On the running dashboard, tap driver rows to select and highlight up to six drivers on the track view.
- Tip: Keep the enclosed display where it has reliable 2.4 GHz Wi-Fi and internet access.
- ⚠ Outside a live F1 session, the public data feed may show the latest available historical session rather than live timing.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| EXT | usb_c_power +5V → Waveshare board USB-C power input | power |
| GND | usb_c_power GND | ground |
Firmware
ESP32#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_http_client.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "cJSON.h"
#define WIFI_SSID "YOUR_WIFI_NAME"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"
#define SCREEN_W 800
#define SCREEN_H 480
#define MAX_CARS 20
#define OPENF1_URL "https://api.openf1.org"
typedef struct { int number; float x; float y; bool selected; char code[5]; char gap[16]; } car_t;
static car_t cars[MAX_CARS];
static int car_count = 0;
static int selected_count = 0;
static uint16_t *framebuffer = NULL;
static i2c_master_dev_handle_t touch_handle = NULL;
static uint16_t color565(uint8_t r, uint8_t g, uint8_t b) { return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); }
static void pixel(int x, int y, uint16_t c) { if (x >= 0 && x < SCREEN_W && y >= 0 && y < SCREEN_H) framebuffer[y * SCREEN_W + x] = c; }
static void fill(int x, int y, int w, int h, uint16_t c) { for (int yy = y; yy < y + h; yy++) for (int xx = x; xx < x + w; xx++) pixel(xx, yy, c); }
static void marker(int x, int y, uint16_t c) { fill(x - 6, y - 6, 13, 13, c); fill(x - 3, y - 3, 7, 7, color565(10, 15, 20)); }
static car_t *find_car(int number) { for (int i = 0; i < car_count; i++) if (cars[i].number == number) return &cars[i]; return NULL; }
static char *http_get(const char *path) {
char url[200]; snprintf(url, sizeof(url), "%s%s", OPENF1_URL, path);
esp_http_client_config_t cfg = {.url = url, .timeout_ms = 12000, .transport_type = HTTP_TRANSPORT_OVER_SSL, .skip_cert_common_name_check = true};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (!client || esp_http_client_open(client, 0) != ESP_OK) { if (client) esp_http_client_cleanup(client); return NULL; }
int length = esp_http_client_fetch_headers(client);
if (length < 1 || length > 150000) { esp_http_client_close(client); esp_http_client_cleanup(client); return NULL; }
char *body = calloc(1, length + 1);
if (!body || esp_http_client_read_response(client, body, length) < 1) { free(body); body = NULL; }
esp_http_client_close(client); esp_http_client_cleanup(client); return body;
}
static void update_openf1(void) {
char *body = http_get("/v1/drivers?session_key=latest");
if (!body) return;
cJSON *list = cJSON_Parse(body); free(body); if (!list) return;
int n = cJSON_GetArraySize(list); car_count = n > MAX_CARS ? MAX_CARS : n;
for (int i = 0; i < car_count; i++) { cJSON *row = cJSON_GetArrayItem(list, i); cJSON *num = cJSON_GetObjectItem(row, "driver_number"); cJSON *name = cJSON_GetObjectItem(row, "name_acronym"); cars[i].number = num ? num->valueint : 0; snprintf(cars[i].code, sizeof(cars[i].code), "%s", cJSON_IsString(name) ? name->valuestring : "UNK"); snprintf(cars[i].gap, sizeof(cars[i].gap), "P%02d", i + 1); }
cJSON_Delete(list);
body = http_get("/v1/intervals?session_key=latest");
if (body) { list = cJSON_Parse(body); free(body); if (list) { for (int i = 0; i < cJSON_GetArraySize(list); i++) { cJSON *row = cJSON_GetArrayItem(list, i); cJSON *num = cJSON_GetObjectItem(row, "driver_number"); cJSON *gap = cJSON_GetObjectItem(row, "gap_to_leader"); car_t *car = num ? find_car(num->valueint) : NULL; if (car && cJSON_IsNumber(gap)) snprintf(car->gap, sizeof(car->gap), "+%.2fs", gap->valuedouble); } cJSON_Delete(list); } }
body = http_get("/v1/location?session_key=latest");
if (body) { list = cJSON_Parse(body); free(body); if (list) { for (int i = 0; i < cJSON_GetArraySize(list); i++) { cJSON *row = cJSON_GetArrayItem(list, i); cJSON *num = cJSON_GetObjectItem(row, "driver_number"); cJSON *x = cJSON_GetObjectItem(row, "x"); cJSON *y = cJSON_GetObjectItem(row, "y"); car_t *car = num ? find_car(num->valueint) : NULL; if (car && cJSON_IsNumber(x) && cJSON_IsNumber(y)) { car->x = x->valuedouble; car->y = y->valuedouble; } } cJSON_Delete(list); } }
}
static void draw_dashboard(void) {
uint16_t bg = color565(9, 14, 20), panel = color565(22, 33, 43), line = color565(69, 86, 98), white = color565(240, 245, 248), red = color565(255, 48, 76);
fill(0, 0, SCREEN_W, SCREEN_H, bg); fill(15, 70, 625, 370, panel); fill(655, 70, 130, 370, panel);
/* A clear track frame remains visible even when the live location feed is unavailable. */
for (int i = 0; i < 530; i++) { int y = 250 + ((i % 160) - 80) * ((i % 2) ? 1 : 0) / 4; pixel(60 + i, y, line); pixel(60 + i, y + 1, line); }
float min_x = 1e9f, max_x = -1e9f, min_y = 1e9f, max_y = -1e9f;
for (int i = 0; i < car_count; i++) if (cars[i].x != 0 || cars[i].y != 0) { if (cars[i].x < min_x) min_x = cars[i].x; if (cars[i].x > max_x) max_x = cars[i].x; if (cars[i].y < min_y) min_y = cars[i].y; if (cars[i].y > max_y) max_y = cars[i].y; }
for (int i = 0; i < car_count; i++) { int x = 60 + (int)((cars[i].x - min_x) * 520.0f / (max_x - min_x + 0.01f)); int y = 100 + (int)((max_y - cars[i].y) * 300.0f / (max_y - min_y + 0.01f)); marker(x, y, cars[i].selected ? red : white); }
/* Driver rows are touch targets: 10 visible rows, with live gaps refreshed every eight seconds. */
for (int i = 0; i < car_count && i < 10; i++) fill(662, 78 + i * 35, 116, 29, cars[i].selected ? red : color565(42, 58, 70));
}
static bool read_touch(int *x, int *y) {
uint8_t reg[2] = {0x81, 0x4E}; uint8_t data[6] = {0};
if (i2c_master_transmit_receive(touch_handle, reg, 2, data, 6, 20) != ESP_OK || !(data[0] & 0x80) || !(data[0] & 0x0F)) return false;
*x = data[2] | (data[3] << 8); *y = data[4] | (data[5] << 8); uint8_t clear[3] = {0x81, 0x4E, 0}; i2c_master_transmit(touch_handle, clear, 3, 20); return true;
}
static void wifi_event(void *arg, esp_event_base_t base, int32_t event, void *data) { if (base == WIFI_EVENT && (event == WIFI_EVENT_STA_START || event == WIFI_EVENT_STA_DISCONNECTED)) esp_wifi_connect(); }
static void start_wifi(void) { esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_sta(); wifi_init_config_t init = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&init); esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event, NULL); wifi_config_t config = {0}; strncpy((char *)config.sta.ssid, WIFI_SSID, sizeof(config.sta.ssid)); strncpy((char *)config.sta.password, WIFI_PASSWORD, sizeof(config.sta.password)); esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(WIFI_IF_STA, &config); esp_wifi_start(); }
void app_main(void) {
ESP_ERROR_CHECK(nvs_flash_init());
gpio_config_t backlight = {.pin_bit_mask = 1ULL << 6, .mode = GPIO_MODE_OUTPUT}; gpio_config(&backlight); gpio_set_level(6, 1);
esp_lcd_rgb_panel_config_t rgb = {.data_width = 16, .bits_per_pixel = 16, .num_fbs = 1, .pclk_gpio_num = 7, .vsync_gpio_num = 3, .hsync_gpio_num = 46, .de_gpio_num = 5, .data_gpio_nums = {14,38,18,17,10,39,0,45,48,47,21,1,2,42,41,40}, .timings = {.pclk_hz = 14000000, .h_res = SCREEN_W, .v_res = SCREEN_H, .hsync_pulse_width = 10, .hsync_back_porch = 43, .hsync_front_porch = 8, .vsync_pulse_width = 8, .vsync_back_porch = 12, .vsync_front_porch = 8}, .flags = {.fb_in_psram = 1, .pclk_active_neg = 1}};
esp_lcd_panel_handle_t lcd; ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&rgb, &lcd)); ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd)); ESP_ERROR_CHECK(esp_lcd_panel_init(lcd)); ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(lcd, 1, (void **)&framebuffer));
i2c_master_bus_handle_t bus; i2c_master_bus_config_t bus_config = {.i2c_port = I2C_NUM_0, .sda_io_num = 8, .scl_io_num = 9, .clk_source = I2C_CLK_SRC_DEFAULT}; ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus)); i2c_device_config_t touch_config = {.device_address = 0x5D, .scl_speed_hz = 400000}; ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &touch_config, &touch_handle));
start_wifi(); draw_dashboard(); int refresh_ticks = 0;
while (true) { int x, y; if (read_touch(&x, &y) && x >= 655 && y >= 78 && y < 428) { int row = (y - 78) / 35; if (row < car_count && row < 10) { if (cars[row].selected) { cars[row].selected = false; selected_count--; } else if (selected_count < 6) { cars[row].selected = true; selected_count++; } draw_dashboard(); } } if (++refresh_ticks >= 80) { update_openf1(); draw_dashboard(); refresh_ticks = 0; } vTaskDelay(pdMS_TO_TICKS(100)); }
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.