6 Commits
40 changed files with 6471 additions and 1415 deletions
+93 -36
View File
@@ -1,6 +1,16 @@
#include "esp_camera.h" #include "esp_camera.h"
#include <WiFi.h> #include <WiFi.h>
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include <ESP32Servo.h>
// === НАСТРОЙКА РЕЖИМА ЗАГЛУШКИ ===
const bool USE_MOCK_IF_NO_CAMERA = true; // true - слать черный экран, false - ничего не слать
Servo myServo;
const int servoPin = 13; // ПРИОРИТЕТ: Пин жестко закреплен за сервоприводом
// !!! УНИКАЛЬНЫЙ ID ДЛЯ КАЖДОЙ ПЛАТЫ
const uint8_t DEVICE_ID = 1;
// Конфигурация пинов (пример для Seeed Studio XIAO ESP32S3 Sense) // Конфигурация пинов (пример для Seeed Studio XIAO ESP32S3 Sense)
#define PWDN_GPIO_NUM -1 #define PWDN_GPIO_NUM -1
@@ -10,7 +20,7 @@
#define SIOC_GPIO_NUM 39 #define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48 #define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 47 #define Y8_GPIO_NUM 47
#define Y7_GPIO_NUM 13 #define Y7_GPIO_NUM 18 // ИЗМЕНЕНО: Отключаем этот пин для камеры, отдаем под Серво
#define Y6_GPIO_NUM 14 #define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 12 #define Y5_GPIO_NUM 12
#define Y4_GPIO_NUM 11 #define Y4_GPIO_NUM 11
@@ -20,24 +30,45 @@
#define HREF_GPIO_NUM 38 #define HREF_GPIO_NUM 38
#define PCLK_GPIO_NUM 37 #define PCLK_GPIO_NUM 37
const char* ssid = "RT-WiFi-1712"; const char* ssid = "RT-WiFi-1712";
const char* password = "yt3xihY2Ci"; const char* password = "yt3xihY2Ci";
const char* serverIP = "192.168.0.11"; const char* serverIP = "192.168.0.10";
const int serverPort = 9999; const int serverPort = 9999;
const int localPort = 8888; const int localPort = 8888;
// !!! УНИКАЛЬНЫЙ ID ДЛЯ КАЖДОЙ ПЛАТЫ (например, 1 для первой, 2 для второй и т.д.)
const uint8_t DEVICE_ID = 1;
WiFiUDP udpVideo; WiFiUDP udpVideo;
WiFiUDP udpCmd; WiFiUDP udpCmd;
camera_config_t config; camera_config_t config;
// Флаг успешной инициализации камеры
bool isCameraInitialized = false;
// Моковый минимальный валидный JPEG черного цвета
const uint8_t mockBlackJpeg[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0x60,
0x00, 0x60, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x08,
0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xFF, 0xC4, 0x00, 0x14,
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0xFF, 0xD9
};
const size_t mockSize = sizeof(mockBlackJpeg);
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
// Настройка камеры (QVGA для стабильности)
// Инициализируем сервопривод в первую очередь
myServo.attach(servoPin, 500, 2500);
Serial.println("Сервопривод инициализирован на пине 13");
// Настройка конфигурации камеры
config.ledc_channel = LEDC_CHANNEL_0; config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0; config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM; config.pin_d0 = Y2_GPIO_NUM;
@@ -45,7 +76,7 @@ void setup() {
config.pin_d2 = Y4_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; // Передаст -1, библиотека не тронет 13 пин
config.pin_d6 = Y8_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM;
@@ -62,7 +93,15 @@ void setup() {
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_count = 2; config.fb_count = 2;
esp_camera_init(&config); // Проверяем статус инициализации камеры
esp_err_t err = esp_camera_init(&config);
if (err == ESP_OK) {
isCameraInitialized = true;
Serial.println("Камера успешно инициализирована!");
} else {
isCameraInitialized = false;
Serial.printf("Камера не найдена (код 0x%x). Режим мок-данных: %s\n", err, USE_MOCK_IF_NO_CAMERA ? "ВКЛ" : "ВЫКЛ");
}
// Подключение к Wi-Fi // Подключение к Wi-Fi
WiFi.begin(ssid, password); WiFi.begin(ssid, password);
@@ -79,43 +118,58 @@ void setup() {
void checkIncomingCommands() { void checkIncomingCommands() {
int packetSize = udpCmd.parsePacket(); int packetSize = udpCmd.parsePacket();
if (packetSize > 0) { if (packetSize == 5) {
char packetBuffer[255]; uint8_t packetBuffer[5];
int len = udpCmd.read(packetBuffer, 255); int len = udpCmd.read(packetBuffer, 5);
if (len > 0) { packetBuffer[len] = 0; } if (len == 5) {
uint8_t steer = packetBuffer[0];
uint8_t throttle = packetBuffer[1];
uint8_t brake = packetBuffer[2];
uint8_t gear = packetBuffer[3];
uint8_t flags = packetBuffer[4];
String command = String(packetBuffer); // ИСПРАВЛЕНО: Инвертирован диапазон (180, 0 вместо 0, 180) для исправления направления
command.trim(); int servoAngle = map(steer, 0, 200, 180, 0);
myServo.write(servoAngle);
Serial.print("Получена команда: "); Serial.printf("Got control packet: Steer=%d (Angle=%d), Throttle=%d%%, Brake=%d%%, Gear=%d, Flags=0x%02X\n",
Serial.println(command); steer, servoAngle, throttle, brake, gear, flags);
Serial.print("Получена команда: ");
Serial.println(command);
// Логика управления
if (command == "left") {
// Ваш код поворота налево
} else if (command == "right") {
// Ваш код поворота направо
} else if (command == "start") {
// Ваш код запуска моторов / логики
} else if (command == "stop") {
// Ваш код остановки
} }
} else if (packetSize > 0) {
char garbage[255];
udpCmd.read(garbage, sizeof(garbage));
} }
} }
void loop() { void loop() {
checkIncomingCommands(); checkIncomingCommands();
camera_fb_t * fb = esp_camera_fb_get(); uint8_t * buf = nullptr;
if (!fb) return; size_t bufferSize = 0;
camera_fb_t * fb = nullptr;
size_t bufferSize = fb->len; // Пытаемся получить кадр, если камера запущена
uint8_t * buf = fb->buf; if (isCameraInitialized) {
fb = esp_camera_fb_get();
if (fb) {
bufferSize = fb->len;
buf = fb->buf;
}
}
// Размер куска данных уменьшаем до 1023, чтобы вместе с 1 байтом ID пакет был 1024 байта // Если кадра с камеры нет
if (!buf) {
// Если отправка заглушки отключена — выходим из цикла, продолжая слушать команды
if (!USE_MOCK_IF_NO_CAMERA) {
delay(10);
return;
}
// Если включена — подставляем моковый черный кадр
bufferSize = mockSize;
buf = (uint8_t *)mockBlackJpeg;
}
// Нарезка и отправка по UDP (оригинальная логика)
size_t chunkSize = 1023; size_t chunkSize = 1023;
uint8_t packetBuffer[1024]; uint8_t packetBuffer[1024];
@@ -125,15 +179,18 @@ void loop() {
sendSize = bufferSize - i; sendSize = bufferSize - i;
} }
// Формируем пакет: первый байт — ID, дальше — видеоданные
packetBuffer[0] = DEVICE_ID; packetBuffer[0] = DEVICE_ID;
memcpy(packetBuffer + 1, buf + i, sendSize); memcpy(packetBuffer + 1, buf + i, sendSize);
udpVideo.beginPacket(serverIP, serverPort); udpVideo.beginPacket(serverIP, serverPort);
udpVideo.write(packetBuffer, sendSize + 1); // Отправляем данные вместе с ID udpVideo.write(packetBuffer, sendSize + 1);
udpVideo.endPacket(); udpVideo.endPacket();
} }
// Возвращаем буфер только если он реальный
if (fb) {
esp_camera_fb_return(fb); esp_camera_fb_return(fb);
}
delay(10); delay(10);
} }
+4
View File
@@ -19,7 +19,11 @@ X0GP_LOG_LEVEL=info
X0GP_TICK_RATE=60 X0GP_TICK_RATE=60
X0GP_SNAPSHOT_RATE=30 X0GP_SNAPSHOT_RATE=30
X0GP_MAX_CLIENTS=100 X0GP_MAX_CLIENTS=100
X0GP_SEED_RACES=0
X0GP_SEED_RESET=0
# Optional: override path(s) to dotenv files (colon-separated).
# X0GP_DOTENV=.env:./configs/local.env
# PoC: skip auth, allow multiple clients on the same track. # PoC: skip auth, allow multiple clients on the same track.
X0GP_DEV_MODE=true X0GP_DEV_MODE=true
+152 -9
View File
@@ -17,9 +17,11 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@@ -73,7 +75,7 @@ func boundsToWire(b catalog.Bounds) transport.TrackBounds {
} }
} }
func trackToWire(t catalog.TrackMeta) transport.TrackWire { func trackToWire(t catalog.TrackMeta, activeTracks map[string]bool) transport.TrackWire {
return transport.TrackWire{ return transport.TrackWire{
ID: t.ID, ID: t.ID,
Name: t.Name, Name: t.Name,
@@ -92,20 +94,36 @@ func trackToWire(t catalog.TrackMeta) transport.TrackWire {
BestLapHolder: t.BestLapHolder, BestLapHolder: t.BestLapHolder,
CreatedMs: t.CreatedMs, CreatedMs: t.CreatedMs,
UpdatedMs: t.UpdatedMs, UpdatedMs: t.UpdatedMs,
IsActive: activeTracks[t.ID],
} }
} }
func tracksToWire(in []catalog.TrackMeta) []transport.TrackWire { func tracksToWire(in []catalog.TrackMeta, activeTracks map[string]bool) []transport.TrackWire {
if len(in) == 0 { if len(in) == 0 {
return []transport.TrackWire{} return []transport.TrackWire{}
} }
out := make([]transport.TrackWire, len(in)) out := make([]transport.TrackWire, len(in))
for i, t := range in { for i, t := range in {
out[i] = trackToWire(t) out[i] = trackToWire(t, activeTracks)
} }
return out return out
} }
func getActiveTracks(ctx context.Context, cat *catalog.Service) map[string]bool {
active := make(map[string]bool)
calendar, err := cat.GetTrackCalendar(ctx)
if err != nil {
return active
}
now := time.Now()
for _, entry := range calendar {
if (now.Equal(entry.StartAt) || now.After(entry.StartAt)) && now.Before(entry.EndAt) {
active[entry.TrackID] = true
}
}
return active
}
func trackFromWire(w transport.TrackWire) catalog.TrackMeta { func trackFromWire(w transport.TrackWire) catalog.TrackMeta {
return catalog.TrackMeta{ return catalog.TrackMeta{
ID: w.ID, ID: w.ID,
@@ -171,6 +189,7 @@ func carToWire(c catalog.CarMeta) transport.CarWire {
ColorHex: c.ColorHex, ColorHex: c.ColorHex,
AvatarURL: c.AvatarURL, AvatarURL: c.AvatarURL,
Active: c.Active, Active: c.Active,
DeviceID: c.DeviceID,
// Stats are read-mostly; the server fills them on read. // Stats are read-mostly; the server fills them on read.
TotalDistanceM: c.TotalDistanceM, TotalDistanceM: c.TotalDistanceM,
TotalRaces: c.TotalRaces, TotalRaces: c.TotalRaces,
@@ -223,6 +242,7 @@ func carFromWire(w transport.CarWire) catalog.CarMeta {
Active: w.Active, Active: w.Active,
CreatedMs: w.CreatedMs, CreatedMs: w.CreatedMs,
UpdatedMs: w.UpdatedMs, UpdatedMs: w.UpdatedMs,
DeviceID: w.DeviceID,
} }
} }
@@ -274,6 +294,7 @@ func carPatchFromWire(p transport.CarPatch) catalog.UpdateCarOptions {
Chassis: chassisFromPtr(p.Chassis), Chassis: chassisFromPtr(p.Chassis),
Motor: motorFromPtr(p.Motor), Motor: motorFromPtr(p.Motor),
Battery: batteryFromPtr(p.Battery), Battery: batteryFromPtr(p.Battery),
DeviceID: p.DeviceID,
} }
} }
@@ -400,10 +421,11 @@ func mapCatalogError(err error) (int, string) {
// @Failure 500 {object} map[string]interface{} "Internal server error" // @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/catalog [get] // @Router /api/catalog [get]
func catalogHandler(svc *catalog.Service) http.HandlerFunc { func catalogHandler(svc *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
snap := svc.Snapshot() snap := svc.Snapshot()
activeTracks := getActiveTracks(r.Context(), svc)
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"tracks": tracksToWire(snap.Tracks), "tracks": tracksToWire(snap.Tracks, activeTracks),
"cars": carsToWire(snap.Cars), "cars": carsToWire(snap.Cars),
"summary": summaryToWire(svc.Stats()), "summary": summaryToWire(svc.Stats()),
}) })
@@ -439,8 +461,9 @@ func tracksHandler(svc *catalog.Service) http.HandlerFunc {
if tag != "" { if tag != "" {
out = filterTracksByTag(out, tag) out = filterTracksByTag(out, tag)
} }
activeTracks := getActiveTracks(r.Context(), svc)
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"tracks": tracksToWire(out), "tracks": tracksToWire(out, activeTracks),
"count": len(out), "count": len(out),
}) })
} }
@@ -471,6 +494,7 @@ func trackByIDHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, http.StatusBadRequest, "bad_request", "missing track id") writeError(w, http.StatusBadRequest, "bad_request", "missing track id")
return return
} }
activeTracks := getActiveTracks(r.Context(), svc)
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
t, ok := svc.GetTrack(id) t, ok := svc.GetTrack(id)
@@ -478,7 +502,7 @@ func trackByIDHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, http.StatusNotFound, "not_found", "track not found") writeError(w, http.StatusNotFound, "not_found", "track not found")
return return
} }
writeJSON(w, http.StatusOK, trackToWire(t)) writeJSON(w, http.StatusOK, trackToWire(t, activeTracks))
case http.MethodPut: case http.MethodPut:
var req transport.TrackUpdateRequest var req transport.TrackUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -495,7 +519,7 @@ func trackByIDHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, status, code, err.Error()) writeError(w, status, code, err.Error())
return return
} }
writeJSON(w, http.StatusOK, transport.TrackAck{Ok: true, ID: updated.ID, Track: trackToWire(updated)}) writeJSON(w, http.StatusOK, transport.TrackAck{Ok: true, ID: updated.ID, Track: trackToWire(updated, activeTracks)})
case http.MethodDelete: case http.MethodDelete:
if err := svc.DeleteTrack(r.Context(), id); err != nil { if err := svc.DeleteTrack(r.Context(), id); err != nil {
status, code := mapCatalogError(err) status, code := mapCatalogError(err)
@@ -549,7 +573,8 @@ func createTrackHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, status, code, err.Error()) writeError(w, status, code, err.Error())
return return
} }
writeJSON(w, http.StatusCreated, transport.TrackAck{Ok: true, ID: created.ID, Track: trackToWire(created)}) activeTracks := getActiveTracks(r.Context(), svc)
writeJSON(w, http.StatusCreated, transport.TrackAck{Ok: true, ID: created.ID, Track: trackToWire(created, activeTracks)})
} }
} }
@@ -734,3 +759,121 @@ func carsRouter(svc *catalog.Service) http.HandlerFunc {
} }
} }
} }
// trackCalendarRouter routes /api/tracks/calendar to GET or POST
func trackCalendarRouter(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getTrackCalendarHandler(cat)(w, r)
case http.MethodPost:
createTrackCalendarHandler(cat)(w, r)
default:
w.Header().Set("Allow", "GET, POST")
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "use GET or POST")
}
}
}
// getTrackCalendarHandler godoc
// @Summary Get Track Calendar
// @Description Returns the full calendar of track active periods.
// @Tags tracks
// @Produce json
// @Success 200 {object} transport.TrackCalendarResponse "List of scheduled active periods"
// @Failure 500 {object} map[string]interface{} "Internal error"
// @Router /api/tracks/calendar [get]
func getTrackCalendarHandler(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
entries, err := cat.GetTrackCalendar(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
items := make([]transport.TrackCalendarEntry, len(entries))
for i, entry := range entries {
items[i] = transport.TrackCalendarEntry{
ID: entry.ID,
TrackID: entry.TrackID,
StartAt: entry.StartAt,
EndAt: entry.EndAt,
}
}
writeJSON(w, http.StatusOK, transport.TrackCalendarResponse{Items: items})
}
}
// createTrackCalendarHandler godoc
// @Summary Schedule Track Active Period
// @Description Schedules an active period for a physical track.
// @Tags tracks
// @Accept json
// @Produce json
// @Param payload body transport.TrackCalendarCreateRequest true "Calendar entry details"
// @Success 201 {object} transport.TrackCalendarEntry "Created calendar entry"
// @Failure 400 {object} map[string]interface{} "Invalid input"
// @Failure 404 {object} map[string]interface{} "Track not found"
// @Failure 500 {object} map[string]interface{} "Internal error"
// @Router /api/tracks/calendar [post]
func createTrackCalendarHandler(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req transport.TrackCalendarCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
entry, err := cat.CreateTrackCalendar(r.Context(), req.TrackID, req.StartAt, req.EndAt)
if err != nil {
if errors.Is(err, catalog.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", err.Error())
return
}
if errors.Is(err, catalog.ErrInvalidInput) {
writeError(w, http.StatusBadRequest, "invalid_input", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
writeJSON(w, http.StatusCreated, transport.TrackCalendarEntry{
ID: entry.ID,
TrackID: entry.TrackID,
StartAt: entry.StartAt,
EndAt: entry.EndAt,
})
}
}
// deleteTrackCalendarHandler godoc
// @Summary Delete Scheduled Period
// @Description Deletes a scheduled active period by ID.
// @Tags tracks
// @Param id path int true "Calendar Entry ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Not found"
// @Failure 500 {object} map[string]interface{} "Internal error"
// @Router /api/tracks/calendar/{id} [delete]
func deleteTrackCalendarHandler(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/api/tracks/calendar/")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id", "id must be an integer")
return
}
err = cat.DeleteTrackCalendar(r.Context(), id)
if err != nil {
if errors.Is(err, catalog.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
}
+10 -5
View File
@@ -84,12 +84,13 @@ func (cb *catalogBroadcaster) broadcast(ev catalog.Event) {
return return
case catalog.EventTrackUpsert, catalog.EventTrackDelete: case catalog.EventTrackUpsert, catalog.EventTrackDelete:
snap := cb.svc.Snapshot() snap := cb.svc.Snapshot()
activeTracks := getActiveTracks(context.Background(), cb.svc)
cb.hub.Publish(&transport.Envelope{ cb.hub.Publish(&transport.Envelope{
Type: transport.TypeTrackSnapshot, Type: transport.TypeTrackSnapshot,
Payload: transport.TrackSnapshot{ Payload: transport.TrackSnapshot{
GeneratedMs: snap.GeneratedMs, GeneratedMs: snap.GeneratedMs,
Version: snap.Version, Version: snap.Version,
Tracks: tracksToWire(snap.Tracks), Tracks: tracksToWire(snap.Tracks, activeTracks),
}, },
}) })
cb.hub.Publish(&transport.Envelope{ cb.hub.Publish(&transport.Envelope{
@@ -139,10 +140,11 @@ func handleTrackWSMessage(c *realtime.Client, svc *catalog.Service, env *transpo
out = filterTracksByTag(out, req.Tag) out = filterTracksByTag(out, req.Tag)
} }
snap := svc.Snapshot() snap := svc.Snapshot()
activeTracks := getActiveTracks(wsCtx(), svc)
sendWS(c, transport.TypeTrackSnapshot, transport.TrackSnapshot{ sendWS(c, transport.TypeTrackSnapshot, transport.TrackSnapshot{
GeneratedMs: snap.GeneratedMs, GeneratedMs: snap.GeneratedMs,
Version: snap.Version, Version: snap.Version,
Tracks: tracksToWire(out), Tracks: tracksToWire(out, activeTracks),
}) })
case transport.TypeTrackGet: case transport.TypeTrackGet:
var req transport.TrackGetRequest var req transport.TrackGetRequest
@@ -156,8 +158,9 @@ func handleTrackWSMessage(c *realtime.Client, svc *catalog.Service, env *transpo
sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: false, ID: req.ID, Error: "not_found"}) sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: false, ID: req.ID, Error: "not_found"})
return return
} }
activeTracks := getActiveTracks(wsCtx(), svc)
sendWS(c, transport.TypeTrackSnapshot, transport.TrackSnapshot{ sendWS(c, transport.TypeTrackSnapshot, transport.TrackSnapshot{
Tracks: []transport.TrackWire{trackToWire(t)}, Tracks: []transport.TrackWire{trackToWire(t, activeTracks)},
}) })
case transport.TypeTrackCreate: case transport.TypeTrackCreate:
var req transport.TrackCreateRequest var req transport.TrackCreateRequest
@@ -173,7 +176,8 @@ func handleTrackWSMessage(c *realtime.Client, svc *catalog.Service, env *transpo
sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: false, Error: err.Error()}) sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: false, Error: err.Error()})
return return
} }
sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: true, ID: created.ID, Track: trackToWire(created)}) activeTracks := getActiveTracks(wsCtx(), svc)
sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: true, ID: created.ID, Track: trackToWire(created, activeTracks)})
case transport.TypeTrackUpdate: case transport.TypeTrackUpdate:
var req transport.TrackUpdateRequest var req transport.TrackUpdateRequest
_ = json.Unmarshal(payloadBytes, &req) _ = json.Unmarshal(payloadBytes, &req)
@@ -183,7 +187,8 @@ func handleTrackWSMessage(c *realtime.Client, svc *catalog.Service, env *transpo
sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: false, ID: req.ID, Error: err.Error()}) sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: false, ID: req.ID, Error: err.Error()})
return return
} }
sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: true, ID: updated.ID, Track: trackToWire(updated)}) activeTracks := getActiveTracks(wsCtx(), svc)
sendWS(c, transport.TypeTrackAck, transport.TrackAck{Ok: true, ID: updated.ID, Track: trackToWire(updated, activeTracks)})
case transport.TypeTrackDelete: case transport.TypeTrackDelete:
var req transport.TrackDeleteRequest var req transport.TrackDeleteRequest
_ = json.Unmarshal(payloadBytes, &req) _ = json.Unmarshal(payloadBytes, &req)
+15 -18
View File
@@ -283,7 +283,7 @@ func driversListHandler(svc *drivers.Service) http.HandlerFunc {
// @Router /api/drivers/{id} [get] // @Router /api/drivers/{id} [get]
// @Router /api/drivers/{id} [put] // @Router /api/drivers/{id} [put]
// @Router /api/drivers/{id} [delete] // @Router /api/drivers/{id} [delete]
// @Router /api/drivers/{id}/device [put] // @Router /api/drivers/{id}/car [put]
func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.HandlerFunc { func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/drivers/") id := strings.TrimPrefix(r.URL.Path, "/api/drivers/")
@@ -291,8 +291,8 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id") writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return return
} }
if strings.HasSuffix(id, "/device") { if strings.HasSuffix(id, "/car") {
driverID := strings.TrimSuffix(id, "/device") driverID := strings.TrimSuffix(id, "/car")
if driverID == "" { if driverID == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id") writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return return
@@ -303,29 +303,26 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
return return
} }
// Read device_id from JSON body or query param // Read car_id from JSON body or query param
var deviceID *int var carID *string
type deviceRequest struct { type carRequest struct {
DeviceID *int `json:"device_id"` CarID *string `json:"car_id"`
} }
var req deviceRequest var req carRequest
if r.ContentLength > 0 { if r.ContentLength > 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err == nil { if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
deviceID = req.DeviceID carID = req.CarID
} }
} }
// Fallback to query parameter if body didn't provide it // Fallback to query parameter if body didn't provide it
if deviceID == nil { if carID == nil {
qVal := r.URL.Query().Get("device_id") qVal := r.URL.Query().Get("car_id")
if qVal != "" { if qVal != "" {
if qVal == "null" { if qVal == "null" {
deviceID = nil carID = nil
} else if idInt, err := strconv.Atoi(qVal); err == nil {
deviceID = &idInt
} else { } else {
writeError(w, http.StatusBadRequest, "bad_request", "invalid device_id query parameter") carID = &qVal
return
} }
} }
} }
@@ -345,8 +342,8 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
_, _ = lobbySvc.AddDriver(driverID, drv.Name, "") _, _ = lobbySvc.AddDriver(driverID, drv.Name, "")
lobbySvc.SetDriverProfile(driverID, drv.Nickname, drv.AvatarURL, drv.ClanID, "") lobbySvc.SetDriverProfile(driverID, drv.Nickname, drv.AvatarURL, drv.ClanID, "")
// Select device in lobby (handles constraints and write-through) // Select car in lobby (handles constraints and write-through)
if err := lobbySvc.SelectDevice(driverID, deviceID); err != nil { if err := lobbySvc.SelectCar(driverID, carID); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error()) writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return return
} }
@@ -0,0 +1,183 @@
package main
import (
"net/http"
"strconv"
"github.com/x0gp/server/internal/races"
"github.com/x0gp/server/internal/transport"
)
// leaderboardHandler godoc
// @Summary Leaderboard
// @Description Returns paginated leaderboard list. Supports overall leaderboard or track-specific leaderboard for the current year.
// @Tags leaderboard
// @Produce json
// @Param track_id query string false "Track ID. If empty, returns overall leaderboard."
// @Param year query int false "Year. Defaults to current year."
// @Param limit query int false "Limit (1..200, default 50)"
// @Param offset query int false "Offset (default 0)"
// @Param X-Driver-Id header string false "Current driver ID. Defaults to driver-seed-001 (mock Alice)."
// @Success 200 {object} transport.LeaderboardResponse "Leaderboard page and current driver position"
// @Failure 400 {object} map[string]interface{} "Invalid input"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/leaderboard [get]
func leaderboardHandler(svc *races.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
trackID := r.URL.Query().Get("track_id")
yearVal := r.URL.Query().Get("year")
var year int
if yearVal != "" {
var err error
year, err = strconv.Atoi(yearVal)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid year")
return
}
}
limit := 50
limitVal := r.URL.Query().Get("limit")
if limitVal != "" {
var err error
limit, err = strconv.Atoi(limitVal)
if err != nil || limit <= 0 {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid limit")
return
}
if limit > 200 {
limit = 200
}
}
offset := 0
offsetVal := r.URL.Query().Get("offset")
if offsetVal != "" {
var err error
offset, err = strconv.Atoi(offsetVal)
if err != nil || offset < 0 {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid offset")
return
}
}
driverID := r.Header.Get("X-Driver-Id")
if driverID == "" {
driverID = "driver-seed-001" // mock current driver
}
res, err := svc.GetLeaderboard(r.Context(), trackID, year, limit, offset, driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
items := make([]transport.LeaderboardEntry, 0, len(res.Items))
for _, item := range res.Items {
items = append(items, leaderboardEntryToWire(item))
}
var curDriver *transport.LeaderboardEntry
if res.CurrentDriver != nil {
wire := leaderboardEntryToWire(*res.CurrentDriver)
curDriver = &wire
}
writeJSON(w, http.StatusOK, transport.LeaderboardResponse{
Items: items,
Total: res.Total,
Limit: limit,
Offset: offset,
CurrentDriver: curDriver,
})
}
}
func leaderboardEntryToWire(e races.LeaderboardEntry) transport.LeaderboardEntry {
var bestPos *int
if e.BestPos != nil {
pos := *e.BestPos
bestPos = &pos
}
var bestTime *int64
if e.BestTimeMs != nil {
val := *e.BestTimeMs
bestTime = &val
}
return transport.LeaderboardEntry{
DriverID: e.DriverID,
Nickname: e.Nickname,
Name: e.Name,
AvatarURL: e.AvatarURL,
ClanID: e.ClanID,
ClanTag: e.ClanTag,
Points: e.Points,
BestPos: bestPos,
BestTimeMs: bestTime,
Rank: e.Rank,
}
}
// calendarLeaderboardHandler godoc
// @Summary Track Calendar Leaderboards
// @Description Returns the list of scheduled tracks with their status (finished/active/planned), top 3 drivers, and current driver position, sorted by calendar start date.
// @Tags leaderboard
// @Produce json
// @Param year query int false "Year. Defaults to current year."
// @Param X-Driver-Id header string false "Current driver ID. Defaults to driver-seed-001 (mock Alice)."
// @Success 200 {object} transport.TrackCalendarLeaderboardResponse "Calendar leaderboards list"
// @Failure 400 {object} map[string]interface{} "Invalid input"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/leaderboard/tracks [get]
func calendarLeaderboardHandler(svc *races.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
yearVal := r.URL.Query().Get("year")
var year int
if yearVal != "" {
var err error
year, err = strconv.Atoi(yearVal)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid year")
return
}
}
driverID := r.Header.Get("X-Driver-Id")
if driverID == "" {
driverID = "driver-seed-001" // mock current driver
}
res, err := svc.GetCalendarLeaderboard(r.Context(), year, driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
items := make([]transport.TrackCalendarLeaderboard, 0, len(res))
for _, item := range res {
podium := make([]transport.LeaderboardEntry, 0, len(item.Podium))
for _, entry := range item.Podium {
podium = append(podium, leaderboardEntryToWire(entry))
}
var curDriver *transport.LeaderboardEntry
if item.CurrentDriver != nil {
wire := leaderboardEntryToWire(*item.CurrentDriver)
curDriver = &wire
}
items = append(items, transport.TrackCalendarLeaderboard{
TrackID: item.TrackID,
TrackName: item.TrackName,
StartAt: item.StartAt,
EndAt: item.EndAt,
Status: item.Status,
Podium: podium,
CurrentDriver: curDriver,
})
}
writeJSON(w, http.StatusOK, transport.TrackCalendarLeaderboardResponse{Items: items})
}
}
+74 -24
View File
@@ -27,11 +27,13 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"log/slog" "log/slog"
"math"
"net" "net"
"net/http" "net/http"
"os" "os"
@@ -184,7 +186,7 @@ func main() {
// Restore the in-memory lobby from Postgres so active races and // Restore the in-memory lobby from Postgres so active races and
// driver presence survive a restart. // driver presence survive a restart.
restoreCtx, restoreCancel := context.WithTimeout(ctx, 5*time.Second) restoreCtx, restoreCancel := context.WithTimeout(ctx, 15*time.Second)
if racesRestored, driversRestored, err := racesSvc.RestoreFromDB(restoreCtx); err != nil { if racesRestored, driversRestored, err := racesSvc.RestoreFromDB(restoreCtx); err != nil {
logger.Warn("restore from db failed", "err", err) logger.Warn("restore from db failed", "err", err)
} else { } else {
@@ -196,6 +198,9 @@ func main() {
clansSvc := clans.NewService(clans.NewPgStore(pool)) clansSvc := clans.NewService(clans.NewPgStore(pool))
driversSvc := drivers.NewService(drivers.NewPgStore(pool)) driversSvc := drivers.NewService(drivers.NewPgStore(pool))
// WebRTC 1-to-1 driver-car proxy service
webrtcSvc := NewWebRTCService(logger, engine, lobbySvc, videoSvc, cat)
// Persist finished races so the /api/races list and keyset pagination // Persist finished races so the /api/races list and keyset pagination
// can serve historical data across restarts. The snapshot is best- // can serve historical data across restarts. The snapshot is best-
// effort: errors are logged but never block the lobby. // effort: errors are logged but never block the lobby.
@@ -280,6 +285,9 @@ func main() {
// UDP video and command receiver. // UDP video and command receiver.
videoSvc.Start(ctx, &wg) videoSvc.Start(ctx, &wg)
// Start WebRTC service
webrtcSvc.Start(ctx, &wg)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler(hub, engine)) mux.HandleFunc("/health", healthHandler(hub, engine))
mux.HandleFunc("/api/version", versionHandler()) mux.HandleFunc("/api/version", versionHandler())
@@ -287,6 +295,8 @@ func main() {
mux.HandleFunc("/api/catalog", catalogHandler(cat)) mux.HandleFunc("/api/catalog", catalogHandler(cat))
mux.HandleFunc("/api/tracks", tracksRouter(cat)) mux.HandleFunc("/api/tracks", tracksRouter(cat))
mux.HandleFunc("/api/tracks/", trackByIDHandler(cat)) mux.HandleFunc("/api/tracks/", trackByIDHandler(cat))
mux.HandleFunc("/api/tracks/calendar", trackCalendarRouter(cat))
mux.HandleFunc("/api/tracks/calendar/", deleteTrackCalendarHandler(cat))
mux.HandleFunc("/api/cars", carsRouter(cat)) mux.HandleFunc("/api/cars", carsRouter(cat))
mux.HandleFunc("/api/cars/", carByIDHandler(cat)) mux.HandleFunc("/api/cars/", carByIDHandler(cat))
mux.HandleFunc("/api/races", racesListHandler(racesSvc)) mux.HandleFunc("/api/races", racesListHandler(racesSvc))
@@ -295,14 +305,20 @@ func main() {
mux.HandleFunc("/api/races/queue/join", racesQueueJoinHandler(racesSvc)) mux.HandleFunc("/api/races/queue/join", racesQueueJoinHandler(racesSvc))
mux.HandleFunc("/api/races/plans", racePlansRouter(racesSvc)) mux.HandleFunc("/api/races/plans", racePlansRouter(racesSvc))
mux.HandleFunc("/api/races/plans/", racePlansDeleteHandler(racesSvc)) mux.HandleFunc("/api/races/plans/", racePlansDeleteHandler(racesSvc))
mux.HandleFunc("/api/leaderboard", leaderboardHandler(racesSvc))
mux.HandleFunc("/api/leaderboard/tracks", calendarLeaderboardHandler(racesSvc))
mux.HandleFunc("/api/clans", clansRouter(clansSvc)) mux.HandleFunc("/api/clans", clansRouter(clansSvc))
mux.HandleFunc("/api/clans/", clansByIDHandler(clansSvc)) mux.HandleFunc("/api/clans/", clansByIDHandler(clansSvc))
mux.HandleFunc("/api/drivers", driversRouter(driversSvc)) mux.HandleFunc("/api/drivers", driversRouter(driversSvc))
mux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc)) mux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc))
mux.HandleFunc("/api/video/stream", videoSvc.StreamHandler()) mux.HandleFunc("/api/video/stream", videoSvc.StreamHandler())
mux.HandleFunc("/api/video/control", videoSvc.ControlHandler()) mux.HandleFunc("/api/video/control", videoSvc.ControlHandler())
mux.HandleFunc("/api/webrtc/connect", webrtcSvc.ConnectHandler())
mux.HandleFunc("/ws", wsHandler(cfg, engine, hub, cat, lobbySvc, driversSvc, videoSvc, logger)) mux.HandleFunc("/ws", wsHandler(cfg, engine, hub, cat, lobbySvc, driversSvc, videoSvc, logger))
// Static client files (resolving TODO)
mux.Handle("/", http.FileServer(http.Dir("./web")))
// Swagger UI + OpenAPI spec. // Swagger UI + OpenAPI spec.
// UI: GET /swagger/index.html // UI: GET /swagger/index.html
// Spec: GET /swagger/doc.json // Spec: GET /swagger/doc.json
@@ -561,7 +577,7 @@ func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *c
case transport.TypeClientHello: case transport.TypeClientHello:
handleClientHello(c, e, cfg, lobbySvc, driversSvc, env) handleClientHello(c, e, cfg, lobbySvc, driversSvc, env)
case transport.TypeJoinRace: case transport.TypeJoinRace:
handleJoinRace(c, e, env) handleJoinRace(c, e, lobbySvc, cat, env)
case transport.TypeLeaveRace: case transport.TypeLeaveRace:
handleLeaveRace(c, e, env) handleLeaveRace(c, e, env)
case transport.TypeInputState: case transport.TypeInputState:
@@ -619,7 +635,7 @@ func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config
c.Send <- mustEncode(transport.TypeServerHello, hello) c.Send <- mustEncode(transport.TypeServerHello, hello)
} }
func handleJoinRace(c *realtime.Client, e *control.Engine, env *transport.Envelope) { func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service, cat *catalog.Service, env *transport.Envelope) {
payload, _ := env.Payload.(map[string]any) payload, _ := env.Payload.(map[string]any)
slot := 0 slot := 0
name := "anon" name := "anon"
@@ -631,7 +647,20 @@ func handleJoinRace(c *realtime.Client, e *control.Engine, env *transport.Envelo
name = v name = v
} }
} }
car, err := e.AddCar(c.ID, name, slot)
// Resolve DeviceID for this driver internally
driverID := c.DriverID
if driverID == "" {
driverID = c.ID
}
var deviceID *int
if d, err := lobbySvc.GetDriver(driverID); err == nil && d.CarID != nil && *d.CarID != "" {
if carMeta, ok := cat.GetCar(*d.CarID); ok {
deviceID = carMeta.DeviceID
}
}
car, err := e.AddCar(c.ID, name, slot, deviceID)
if err != nil { if err != nil {
sendError(c, "join_failed", err.Error()) sendError(c, "join_failed", err.Error())
return return
@@ -660,33 +689,54 @@ func handleInput(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service,
if v, ok := payload["gear"].(float64); ok { if v, ok := payload["gear"].(float64); ok {
in.Gear = int(v) in.Gear = int(v)
} }
if v, ok := payload["buttons"].(float64); ok {
in.Buttons = uint32(v)
}
car := e.GetCarByDriver(c.ID) car := e.GetCarByDriver(c.ID)
if car == nil { if car == nil {
return return
} }
car.ApplyInput(in, 1.0/60.0) car.ApplyInput(in, 1.0/60.0)
// Forward command transitions to ESP32 physical car if driver is assigned one // Forward command transitions to ESP32 physical car if car has a device_id assigned
driverID := c.DriverID if car.DeviceID != nil {
if driverID == "" { var packet [5]byte
driverID = c.ID
}
if d, err := lobbySvc.GetDriver(driverID); err == nil && d.DeviceID != nil {
var cmd string
if in.Throttle > 0.1 {
cmd = "start"
} else {
cmd = "stop"
}
if in.Steering < -0.2 {
cmd = "left"
} else if in.Steering > 0.2 {
cmd = "right"
}
if cmd != "" && cmd != c.LastUDPCommand { // 1. Steer: mapping float64 [-1.0, 1.0] to [0..200] (100 is center)
c.LastUDPCommand = cmd steerVal := int(math.Round((in.Steering + 1.0) * 100.0))
_ = videoSvc.SendCommand(uint8(*d.DeviceID), cmd) if steerVal < 0 { steerVal = 0 }
if steerVal > 200 { steerVal = 200 }
packet[0] = uint8(steerVal)
// 2. Throttle: mapping float64 [0.0, 1.0] to [0..100]
throttleVal := int(math.Round(in.Throttle * 100.0))
if throttleVal < 0 { throttleVal = 0 }
if throttleVal > 100 { throttleVal = 100 }
packet[1] = uint8(throttleVal)
// 3. Brake: mapping float64 [0.0, 1.0] to [0..100]
brakeVal := int(math.Round(in.Brake * 100.0))
if brakeVal < 0 { brakeVal = 0 }
if brakeVal > 100 { brakeVal = 100 }
packet[2] = uint8(brakeVal)
// 4. Gear: map -1 to 0 (R), 0 to 1 (N), >=1 to 2 (D)
gearVal := 1 // Neutral by default
if in.Gear < 0 {
gearVal = 0 // R
} else if in.Gear > 0 {
gearVal = 2 // D
}
packet[3] = uint8(gearVal)
// 5. Flags: bit 0 (DRS), bit 1 (Pit Limiter)
packet[4] = uint8(in.Buttons & 0x03)
// Deduplicate UDP transmissions
hexCmd := hex.EncodeToString(packet[:])
if hexCmd != c.LastUDPCommand {
c.LastUDPCommand = hexCmd
_ = videoSvc.SendControlPacket(uint8(*car.DeviceID), packet)
} }
} }
+138 -18
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@@ -172,7 +173,6 @@ func (s *UDPVideoService) Start(ctx context.Context, wg *sync.WaitGroup) {
} }
s.mu.RUnlock() s.mu.RUnlock()
} }
if !hasDevice { if !hasDevice {
s.logger.Warn("cannot send command: no ESP32 devices discovered yet") s.logger.Warn("cannot send command: no ESP32 devices discovered yet")
continue continue
@@ -188,13 +188,12 @@ func (s *UDPVideoService) Start(ctx context.Context, wg *sync.WaitGroup) {
}() }()
} }
// SendCommand sends a command string to the ESP32 representing deviceID. // SendControlPacket sends a 5-byte binary control packet to the ESP32 representing deviceID.
func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error { func (s *UDPVideoService) SendControlPacket(deviceID uint8, packet [5]byte) error {
s.mu.RLock() s.mu.RLock()
dev, ok := s.devices[deviceID] dev, ok := s.devices[deviceID]
conn := s.conn conn := s.conn
s.mu.RUnlock() s.mu.RUnlock()
if !ok || dev.addr == nil { if !ok || dev.addr == nil {
return fmt.Errorf("ESP32 device %d address unknown (no video packets received yet)", deviceID) return fmt.Errorf("ESP32 device %d address unknown (no video packets received yet)", deviceID)
} }
@@ -207,10 +206,35 @@ func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error {
Port: s.espCmdPort, Port: s.espCmdPort,
} }
_, err := conn.WriteToUDP([]byte(cmd), cmdAddr) _, err := conn.WriteToUDP(packet[:], cmdAddr)
return err return err
} }
// SendCommand sends a command string to the ESP32 representing deviceID by mapping it to a 5-byte packet.
func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error {
var packet [5]byte
packet[0] = 100 // Steer: center
packet[1] = 0 // Throttle: 0%
packet[2] = 0 // Brake: 0%
packet[3] = 1 // Gear: Neutral
packet[4] = 0 // Flags: 0
switch cmd {
case "start":
packet[1] = 100 // Throttle: 100%
packet[3] = 2 // Gear: Drive
case "stop":
packet[2] = 100 // Brake: 100%
packet[3] = 1 // Gear: Neutral
case "left":
packet[0] = 0 // Steer: full left
case "right":
packet[0] = 200 // Steer: full right
}
return s.SendControlPacket(deviceID, packet)
}
func (s *UDPVideoService) broadcastFrame(deviceID uint8, frame []byte) { func (s *UDPVideoService) broadcastFrame(deviceID uint8, frame []byte) {
// dev.latestFrame is updated inside the caller's lock (Start loop) to avoid races // dev.latestFrame is updated inside the caller's lock (Start loop) to avoid races
if dev, ok := s.devices[deviceID]; ok { if dev, ok := s.devices[deviceID]; ok {
@@ -328,13 +352,14 @@ func (s *UDPVideoService) StreamHandler() http.HandlerFunc {
// ControlHandler godoc // ControlHandler godoc
// @Summary Send command to ESP32 // @Summary Send command to ESP32
// @Description Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888). // @Description Sends a text command or a JSON control packet to the ESP32 command port (UDP 8888).
// @Tags video // @Tags video
// @Param id query int false "Device ID (0..127)" // @Param id query int false "Device ID (0..127)"
// @Param cmd query string true "Command text" // @Param cmd query string false "Command text (legacy)"
// @Param body body map[string]int false "Control packet JSON (steer, throttle, brake, gear, flags)"
// @Produce json // @Produce json
// @Success 200 {object} map[string]interface{} "Command sent" // @Success 200 {object} map[string]interface{} "Command sent"
// @Failure 400 {string} string "Missing cmd parameter" // @Failure 400 {string} string "Missing parameter or invalid body"
// @Failure 500 {string} string "Internal error" // @Failure 500 {string} string "Internal error"
// @Router /api/video/control [post] // @Router /api/video/control [post]
func (s *UDPVideoService) ControlHandler() http.HandlerFunc { func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
@@ -343,14 +368,6 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed") writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed")
return return
} }
cmd := r.URL.Query().Get("cmd")
if cmd == "" {
cmd = r.FormValue("cmd")
}
if cmd == "" {
writeError(w, http.StatusBadRequest, "bad_request", "Missing 'cmd' parameter")
return
}
idVal := r.URL.Query().Get("id") idVal := r.URL.Query().Get("id")
if idVal == "" { if idVal == "" {
@@ -366,7 +383,109 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
} }
} }
err := s.SendCommand(deviceID, cmd) var packet [5]byte
packet[0] = 100 // Steer: center
packet[1] = 0 // Throttle: 0%
packet[2] = 0 // Brake: 0%
packet[3] = 1 // Gear: Neutral
packet[4] = 0 // Flags: 0
isJSON := strings.Contains(r.Header.Get("Content-Type"), "application/json")
var cmdUsed string
if isJSON {
var req struct {
Steer *int `json:"steer"`
Throttle *int `json:"throttle"`
Brake *int `json:"brake"`
Gear *int `json:"gear"`
Flags *int `json:"flags"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", "invalid JSON payload")
return
}
if req.Steer != nil {
val := *req.Steer
if val < 0 {
val = 0
}
if val > 200 {
val = 200
}
packet[0] = uint8(val)
}
if req.Throttle != nil {
val := *req.Throttle
if val < 0 {
val = 0
}
if val > 100 {
val = 100
}
packet[1] = uint8(val)
}
if req.Brake != nil {
val := *req.Brake
if val < 0 {
val = 0
}
if val > 100 {
val = 100
}
packet[2] = uint8(val)
}
if req.Gear != nil {
val := *req.Gear
if val < 0 {
val = 0
}
if val > 2 {
val = 2
}
packet[3] = uint8(val)
}
if req.Flags != nil {
val := *req.Flags
if val < 0 {
val = 0
}
if val > 255 {
val = 255
}
packet[4] = uint8(val)
}
cmdUsed = fmt.Sprintf("json(steer=%d,throttle=%d,brake=%d,gear=%d,flags=%d)", packet[0], packet[1], packet[2], packet[3], packet[4])
} else {
cmd := r.URL.Query().Get("cmd")
if cmd == "" {
cmd = r.FormValue("cmd")
}
if cmd == "" {
writeError(w, http.StatusBadRequest, "bad_request", "Missing 'cmd' parameter")
return
}
cmdUsed = cmd
switch cmd {
case "start":
packet[1] = 100
packet[3] = 2
case "stop":
packet[2] = 100
packet[3] = 1
case "left":
packet[0] = 0
case "right":
packet[0] = 200
default:
writeError(w, http.StatusBadRequest, "bad_request", "Unknown command: "+cmd)
return
}
}
err := s.SendControlPacket(deviceID, packet)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, "internal_error", err.Error()) writeError(w, http.StatusInternalServerError, "internal_error", err.Error())
return return
@@ -375,7 +494,8 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"status": "success", "status": "success",
"device_id": deviceID, "device_id": deviceID,
"command": cmd, "command": cmdUsed,
"packet": packet[:],
}) })
} }
} }
+421
View File
@@ -0,0 +1,421 @@
package main
import (
"context"
"encoding/hex"
"encoding/json"
"log/slog"
"math"
"net/http"
"sync"
"time"
"github.com/pion/webrtc/v4"
"github.com/x0gp/server/internal/catalog"
"github.com/x0gp/server/internal/control"
"github.com/x0gp/server/internal/lobby"
"github.com/x0gp/server/internal/transport"
)
type driverSession struct {
driverID string
deviceID uint8
pc *webrtc.PeerConnection
videoDC *webrtc.DataChannel
dataDC *webrtc.DataChannel
lastCommand string
cancelCtx context.CancelFunc
}
type WebRTCService struct {
logger *slog.Logger
engine *control.Engine
lobbySvc *lobby.Service
videoSvc *UDPVideoService
catalogSvc *catalog.Service
sessions map[string]*driverSession
sessionsMu sync.RWMutex
}
func NewWebRTCService(logger *slog.Logger, e *control.Engine, lobbySvc *lobby.Service, videoSvc *UDPVideoService, catalogSvc *catalog.Service) *WebRTCService {
return &WebRTCService{
logger: logger,
engine: e,
lobbySvc: lobbySvc,
videoSvc: videoSvc,
catalogSvc: catalogSvc,
sessions: make(map[string]*driverSession),
}
}
func (s *WebRTCService) Start(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
s.CloseAll()
return
case <-ticker.C:
s.broadcastTelemetry()
}
}
}()
}
// ConnectHandler godoc
// @Summary Establish WebRTC connection for driver-car 1-to-1 proxying
// @Description Accepts SDP Offer, negotiates peer connection, and returns SDP Answer. Routes video and control data via DataChannels.
// @Tags webrtc
// @Accept json
// @Produce json
// @Param body body transport.WebRTCConnectRequest true "SDP Offer details"
// @Success 200 {object} transport.WebRTCConnectResponse "SDP Answer negotiation success"
// @Router /api/webrtc/connect [post]
func (s *WebRTCService) ConnectHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed")
return
}
var req transport.WebRTCConnectRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request JSON")
return
}
if req.DriverID == "" {
writeError(w, http.StatusBadRequest, "bad_request", "driver_id is required")
return
}
// Resolve device ID for this driver
deviceID := uint8(1) // default fallback
if driver, err := s.lobbySvc.GetDriver(req.DriverID); err == nil && driver.CarID != nil && *driver.CarID != "" {
if car, ok := s.catalogSvc.GetCar(*driver.CarID); ok && car.DeviceID != nil {
deviceID = uint8(*car.DeviceID)
} else {
s.logger.Warn("webrtc client connected but car has no physical device_id assigned in catalog. Defaulting to device 1", "driver_id", req.DriverID, "car_id", *driver.CarID)
}
} else {
s.logger.Warn("webrtc client connected but no car_id assigned in lobbySvc. Defaulting to device 1", "driver_id", req.DriverID)
}
// Create PeerConnection
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
pc, err := webrtc.NewPeerConnection(config)
if err != nil {
s.logger.Error("failed to create webrtc peer connection", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", err.Error())
return
}
// Prepare context for background video pumping
connCtx, connCancel := context.WithCancel(context.Background())
session := &driverSession{
driverID: req.DriverID,
deviceID: deviceID,
pc: pc,
cancelCtx: connCancel,
}
// Handle Data Channels
pc.OnDataChannel(func(d *webrtc.DataChannel) {
s.logger.Info("received remote data channel", "label", d.Label(), "driver_id", req.DriverID)
s.sessionsMu.Lock()
if d.Label() == "video" {
session.videoDC = d
} else if d.Label() == "data" {
session.dataDC = d
}
s.sessionsMu.Unlock()
d.OnOpen(func() {
s.logger.Info("data channel opened", "label", d.Label(), "driver_id", req.DriverID)
if d.Label() == "video" {
// Start pumping video frames from UDPVideoService to the data channel
go s.pumpVideo(connCtx, session)
}
})
d.OnMessage(func(msg webrtc.DataChannelMessage) {
if d.Label() == "data" {
s.handleDataChannelMessage(session, msg.Data)
}
})
})
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
s.logger.Info("webrtc connection state changed", "state", state.String(), "driver_id", req.DriverID)
if state == webrtc.PeerConnectionStateClosed || state == webrtc.PeerConnectionStateFailed {
s.CloseSession(req.DriverID)
}
})
// Set the remote SessionDescription
err = pc.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer,
SDP: req.SDP,
})
if err != nil {
pc.Close()
connCancel()
s.logger.Error("failed to set remote description", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", err.Error())
return
}
// Create Answer
answer, err := pc.CreateAnswer(nil)
if err != nil {
pc.Close()
connCancel()
s.logger.Error("failed to create answer", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", err.Error())
return
}
// Set Local Description
err = pc.SetLocalDescription(answer)
if err != nil {
pc.Close()
connCancel()
s.logger.Error("failed to set local description", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", err.Error())
return
}
// Wait for ICE Gathering to complete (Vanilla ICE)
gatherComplete := webrtc.GatheringCompletePromise(pc)
<-gatherComplete
// Save Session
s.sessionsMu.Lock()
// Clean up existing session if any
if existing, ok := s.sessions[req.DriverID]; ok {
existing.pc.Close()
existing.cancelCtx()
}
s.sessions[req.DriverID] = session
s.sessionsMu.Unlock()
localDesc := pc.LocalDescription()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(transport.WebRTCConnectResponse{
SDP: localDesc.SDP,
Type: "answer",
})
}
}
func (s *WebRTCService) pumpVideo(ctx context.Context, session *driverSession) {
s.logger.Info("starting WebRTC video pump for driver", "driver_id", session.driverID, "device_id", session.deviceID)
frameCh := make(chan []byte, 10)
s.videoSvc.registerClient(session.deviceID, frameCh)
defer func() {
s.videoSvc.unregisterClient(session.deviceID, frameCh)
s.logger.Info("stopping WebRTC video pump for driver", "driver_id", session.driverID)
}()
for {
select {
case <-ctx.Done():
return
case frame, ok := <-frameCh:
if !ok {
return
}
s.sessionsMu.RLock()
dc := session.videoDC
s.sessionsMu.RUnlock()
if dc != nil && dc.ReadyState() == webrtc.DataChannelStateOpen {
if err := dc.Send(frame); err != nil {
s.logger.Debug("failed to send video frame over datachannel", "driver_id", session.driverID, "err", err)
}
}
}
}
}
func (s *WebRTCService) handleDataChannelMessage(session *driverSession, data []byte) {
var payload map[string]any
if err := json.Unmarshal(data, &payload); err != nil {
s.logger.Warn("received invalid JSON on data channel", "driver_id", session.driverID, "err", err)
return
}
// 1. Steering & Throttle inputs
in := transport.InputState{}
hasInput := false
if v, ok := payload["throttle"].(float64); ok {
in.Throttle = v
hasInput = true
}
if v, ok := payload["steering"].(float64); ok {
in.Steering = v
hasInput = true
}
if v, ok := payload["brake"].(float64); ok {
in.Brake = v
hasInput = true
}
if v, ok := payload["gear"].(float64); ok {
in.Gear = int(v)
hasInput = true
}
if v, ok := payload["buttons"].(float64); ok {
in.Buttons = uint32(v)
hasInput = true
}
if hasInput {
s.logger.Debug("received inputs on webrtc data channel", "driver_id", session.driverID, "throttle", in.Throttle, "steering", in.Steering)
car := s.engine.GetCarByDriver(session.driverID)
if car != nil {
car.ApplyInput(in, 1.0/60.0)
}
// Resolve destination device
var destDeviceID *uint8
if car != nil && car.DeviceID != nil {
val := uint8(*car.DeviceID)
destDeviceID = &val
} else if session.deviceID != 0 {
// Fallback to connection-time device
destDeviceID = &session.deviceID
}
if destDeviceID != nil {
var packet [5]byte
// 1. Steer: mapping float64 [-1.0, 1.0] to [0..200] (100 is center)
steerVal := int(math.Round((in.Steering + 1.0) * 100.0))
if steerVal < 0 { steerVal = 0 }
if steerVal > 200 { steerVal = 200 }
packet[0] = uint8(steerVal)
// 2. Throttle: mapping float64 [0.0, 1.0] to [0..100]
throttleVal := int(math.Round(in.Throttle * 100.0))
if throttleVal < 0 { throttleVal = 0 }
if throttleVal > 100 { throttleVal = 100 }
packet[1] = uint8(throttleVal)
// 3. Brake: mapping float64 [0.0, 1.0] to [0..100]
brakeVal := int(math.Round(in.Brake * 100.0))
if brakeVal < 0 { brakeVal = 0 }
if brakeVal > 100 { brakeVal = 100 }
packet[2] = uint8(brakeVal)
// 4. Gear: map -1 to 0 (R), 0 to 1 (N), >=1 to 2 (D)
gearVal := 1 // Neutral by default
if in.Gear < 0 {
gearVal = 0 // R
} else if in.Gear > 0 {
gearVal = 2 // D
}
packet[3] = uint8(gearVal)
// 5. Flags: bit 0 (DRS), bit 1 (Pit Limiter)
packet[4] = uint8(in.Buttons & 0x03)
hexCmd := hex.EncodeToString(packet[:])
if hexCmd != session.lastCommand {
s.logger.Info("webrtc control state transition", "driver_id", session.driverID, "old_hex", session.lastCommand, "new_hex", hexCmd)
session.lastCommand = hexCmd
err := s.videoSvc.SendControlPacket(*destDeviceID, packet)
if err != nil {
s.logger.Error("failed to forward control packet to ESP32", "device_id", *destDeviceID, "packet", packet, "err", err)
} else {
s.logger.Info("forwarded control packet to ESP32 via UDP", "device_id", *destDeviceID, "packet", packet)
}
}
}
}
// Echo back message / send ACK
s.sessionsMu.RLock()
dc := session.dataDC
s.sessionsMu.RUnlock()
if dc != nil && dc.ReadyState() == webrtc.DataChannelStateOpen {
ack, _ := json.Marshal(map[string]any{
"type": "ack",
"applied": true,
"timestamp": time.Now().UnixMilli(),
})
_ = dc.SendText(string(ack))
}
}
func (s *WebRTCService) broadcastTelemetry() {
s.sessionsMu.RLock()
defer s.sessionsMu.RUnlock()
for _, session := range s.sessions {
dc := session.dataDC
if dc != nil && dc.ReadyState() == webrtc.DataChannelStateOpen {
// Pull some stats/telemetry
car := s.engine.GetCarByDriver(session.driverID)
var speed float64
var x, y float64
var carID string
if car != nil {
speed = car.Speed
x = car.X
y = car.Y
carID = car.ID
}
telemetry := map[string]any{
"type": "telemetry",
"car_id": carID,
"driver_id": session.driverID,
"speed": speed,
"x": x,
"y": y,
"timestamp": time.Now().UnixMilli(),
}
payload, _ := json.Marshal(telemetry)
_ = dc.SendText(string(payload))
}
}
}
func (s *WebRTCService) CloseSession(driverID string) {
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()
if session, ok := s.sessions[driverID]; ok {
s.logger.Info("closing WebRTC session for driver", "driver_id", driverID)
session.pc.Close()
session.cancelCtx()
delete(s.sessions, driverID)
}
}
func (s *WebRTCService) CloseAll() {
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()
for driverID, session := range s.sessions {
s.logger.Info("closing WebRTC session (shutdown)", "driver_id", driverID)
session.pc.Close()
session.cancelCtx()
}
s.sessions = make(map[string]*driverSession)
}
+485 -6
View File
@@ -948,7 +948,7 @@ const docTemplate = `{
} }
} }
}, },
"/api/drivers/{id}/device": { "/api/drivers/{id}/car": {
"put": { "put": {
"description": "Dispatches on HTTP method. Path ` + "`" + `/api/drivers/by-nick/{nick}` + "`" + ` is a convenience that returns the driver by 3-letter nickname.", "description": "Dispatches on HTTP method. Path ` + "`" + `/api/drivers/by-nick/{nick}` + "`" + ` is a convenience that returns the driver by 3-letter nickname.",
"produces": [ "produces": [
@@ -1012,6 +1012,120 @@ const docTemplate = `{
} }
} }
}, },
"/api/leaderboard": {
"get": {
"description": "Returns paginated leaderboard list. Supports overall leaderboard or track-specific leaderboard for the current year.",
"produces": [
"application/json"
],
"tags": [
"leaderboard"
],
"summary": "Leaderboard",
"parameters": [
{
"type": "string",
"description": "Track ID. If empty, returns overall leaderboard.",
"name": "track_id",
"in": "query"
},
{
"type": "integer",
"description": "Year. Defaults to current year.",
"name": "year",
"in": "query"
},
{
"type": "integer",
"description": "Limit (1..200, default 50)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset (default 0)",
"name": "offset",
"in": "query"
},
{
"type": "string",
"description": "Current driver ID. Defaults to driver-seed-001 (mock Alice).",
"name": "X-Driver-Id",
"in": "header"
}
],
"responses": {
"200": {
"description": "Leaderboard page and current driver position",
"schema": {
"$ref": "#/definitions/transport.LeaderboardResponse"
}
},
"400": {
"description": "Invalid input",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/leaderboard/tracks": {
"get": {
"description": "Returns the list of scheduled tracks with their status (finished/active/planned), top 3 drivers, and current driver position, sorted by calendar start date.",
"produces": [
"application/json"
],
"tags": [
"leaderboard"
],
"summary": "Track Calendar Leaderboards",
"parameters": [
{
"type": "integer",
"description": "Year. Defaults to current year.",
"name": "year",
"in": "query"
},
{
"type": "string",
"description": "Current driver ID. Defaults to driver-seed-001 (mock Alice).",
"name": "X-Driver-Id",
"in": "header"
}
],
"responses": {
"200": {
"description": "Calendar leaderboards list",
"schema": {
"$ref": "#/definitions/transport.TrackCalendarLeaderboardResponse"
}
},
"400": {
"description": "Invalid input",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/races": { "/api/races": {
"get": { "get": {
"description": "Returns one keyset page of races. Live races (status lobby|racing) come from in-memory lobby.Service; finished races come from Postgres. The two sources are merged and ordered by (started_ms DESC, id ASC). The cursor is opaque: pass back the next_cursor verbatim from a previous page.\n## Filters\n- ` + "`" + `status` + "`" + ` (comma-separated, e.g. \"racing,finished\"). Recognised values: all, lobby, racing, finished.\n- ` + "`" + `track_id` + "`" + ` (exact match against a physical track id; custom lobbies are not supported in this build).\n- ` + "`" + `limit` + "`" + ` (1..200, default 50)", "description": "Returns one keyset page of races. Live races (status lobby|racing) come from in-memory lobby.Service; finished races come from Postgres. The two sources are merged and ordered by (started_ms DESC, id ASC). The cursor is opaque: pass back the next_cursor verbatim from a previous page.\n## Filters\n- ` + "`" + `status` + "`" + ` (comma-separated, e.g. \"racing,finished\"). Recognised values: all, lobby, racing, finished.\n- ` + "`" + `track_id` + "`" + ` (exact match against a physical track id; custom lobbies are not supported in this build).\n- ` + "`" + `limit` + "`" + ` (1..200, default 50)",
@@ -1520,6 +1634,123 @@ const docTemplate = `{
} }
} }
}, },
"/api/tracks/calendar": {
"get": {
"description": "Returns the full calendar of track active periods.",
"produces": [
"application/json"
],
"tags": [
"tracks"
],
"summary": "Get Track Calendar",
"responses": {
"200": {
"description": "List of scheduled active periods",
"schema": {
"$ref": "#/definitions/transport.TrackCalendarResponse"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"post": {
"description": "Schedules an active period for a physical track.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"tracks"
],
"summary": "Schedule Track Active Period",
"parameters": [
{
"description": "Calendar entry details",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/transport.TrackCalendarCreateRequest"
}
}
],
"responses": {
"201": {
"description": "Created calendar entry",
"schema": {
"$ref": "#/definitions/transport.TrackCalendarEntry"
}
},
"400": {
"description": "Invalid input",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Track not found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/tracks/calendar/{id}": {
"delete": {
"description": "Deletes a scheduled active period by ID.",
"tags": [
"tracks"
],
"summary": "Delete Scheduled Period",
"parameters": [
{
"type": "integer",
"description": "Calendar Entry ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/tracks/{id}": { "/api/tracks/{id}": {
"get": { "get": {
"description": "Dispatches on HTTP method. The id is taken from the URL path (` + "`" + `/api/tracks/{id}` + "`" + `). System tracks are immutable and cannot be updated or deleted.", "description": "Dispatches on HTTP method. The id is taken from the URL path (` + "`" + `/api/tracks/{id}` + "`" + `). System tracks are immutable and cannot be updated or deleted.",
@@ -1772,7 +2003,7 @@ const docTemplate = `{
}, },
"/api/video/control": { "/api/video/control": {
"post": { "post": {
"description": "Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888).", "description": "Sends a text command or a JSON control packet to the ESP32 command port (UDP 8888).",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -1789,10 +2020,20 @@ const docTemplate = `{
}, },
{ {
"type": "string", "type": "string",
"description": "Command text", "description": "Command text (legacy)",
"name": "cmd", "name": "cmd",
"in": "query", "in": "query"
"required": true },
{
"description": "Control packet JSON (steer, throttle, brake, gear, flags)",
"name": "body",
"in": "body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
} }
], ],
"responses": { "responses": {
@@ -1804,7 +2045,7 @@ const docTemplate = `{
} }
}, },
"400": { "400": {
"description": "Missing cmd parameter", "description": "Missing parameter or invalid body",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1846,6 +2087,40 @@ const docTemplate = `{
} }
} }
}, },
"/api/webrtc/connect": {
"post": {
"description": "Accepts SDP Offer, negotiates peer connection, and returns SDP Answer. Routes video and control data via DataChannels.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"webrtc"
],
"summary": "Establish WebRTC connection for driver-car 1-to-1 proxying",
"parameters": [
{
"description": "SDP Offer details",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/transport.WebRTCConnectRequest"
}
}
],
"responses": {
"200": {
"description": "SDP Answer negotiation success",
"schema": {
"$ref": "#/definitions/transport.WebRTCConnectResponse"
}
}
}
}
},
"/health": { "/health": {
"get": { "get": {
"description": "Returns 200 OK with current engine phase, WS connection count and total snapshot drops. Intended for load-balancer health checks.", "description": "Returns 200 OK with current engine phase, WS connection count and total snapshot drops. Intended for load-balancer health checks.",
@@ -1967,6 +2242,9 @@ const docTemplate = `{
"color_hex": { "color_hex": {
"type": "string" "type": "string"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2034,6 +2312,9 @@ const docTemplate = `{
"created_ms": { "created_ms": {
"type": "integer" "type": "integer"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2281,6 +2562,78 @@ const docTemplate = `{
} }
} }
}, },
"transport.LeaderboardEntry": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string",
"example": "https://cdn.example.com/u/ace.png"
},
"best_pos": {
"description": "Only populated for track-specific leaderboard",
"type": "integer",
"example": 1
},
"best_time_ms": {
"type": "integer",
"example": 15000
},
"clan_id": {
"type": "string",
"example": "clan-seed-001"
},
"clan_tag": {
"type": "string",
"example": "ACE"
},
"driver_id": {
"type": "string",
"example": "driver-seed-001"
},
"name": {
"type": "string",
"example": "Alice"
},
"nickname": {
"type": "string",
"example": "ACE"
},
"points": {
"type": "integer",
"example": 25
},
"rank": {
"type": "integer",
"example": 1
}
}
},
"transport.LeaderboardResponse": {
"type": "object",
"properties": {
"current_driver": {
"$ref": "#/definitions/transport.LeaderboardEntry"
},
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.LeaderboardEntry"
}
},
"limit": {
"type": "integer",
"example": 50
},
"offset": {
"type": "integer",
"example": 0
},
"total": {
"type": "integer",
"example": 8
}
}
},
"transport.LobbyRace": { "transport.LobbyRace": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2741,6 +3094,100 @@ const docTemplate = `{
} }
} }
}, },
"transport.TrackCalendarCreateRequest": {
"type": "object",
"properties": {
"end_at": {
"type": "string",
"example": "2026-07-30T00:00:00Z"
},
"start_at": {
"type": "string",
"example": "2026-07-15T00:00:00Z"
},
"track_id": {
"type": "string",
"example": "monaco"
}
}
},
"transport.TrackCalendarEntry": {
"type": "object",
"properties": {
"end_at": {
"type": "string",
"example": "2026-07-30T00:00:00Z"
},
"id": {
"type": "integer",
"example": 1
},
"start_at": {
"type": "string",
"example": "2026-07-15T00:00:00Z"
},
"track_id": {
"type": "string",
"example": "monaco"
}
}
},
"transport.TrackCalendarLeaderboard": {
"type": "object",
"properties": {
"current_driver": {
"$ref": "#/definitions/transport.LeaderboardEntry"
},
"end_at": {
"type": "string",
"example": "2026-07-30T00:00:00Z"
},
"podium": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.LeaderboardEntry"
}
},
"start_at": {
"type": "string",
"example": "2026-07-15T00:00:00Z"
},
"status": {
"type": "string",
"example": "active"
},
"track_id": {
"type": "string",
"example": "monaco"
},
"track_name": {
"type": "string",
"example": "Monaco"
}
}
},
"transport.TrackCalendarLeaderboardResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.TrackCalendarLeaderboard"
}
}
}
},
"transport.TrackCalendarResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.TrackCalendarEntry"
}
}
}
},
"transport.TrackCreateRequest": { "transport.TrackCreateRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2851,6 +3298,10 @@ const docTemplate = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"is_active": {
"type": "boolean",
"example": true
},
"lane_width_m": { "lane_width_m": {
"type": "number" "type": "number"
}, },
@@ -2891,6 +3342,34 @@ const docTemplate = `{
"example": "0.1.0-poc" "example": "0.1.0-poc"
} }
} }
},
"transport.WebRTCConnectRequest": {
"type": "object",
"properties": {
"driver_id": {
"type": "string",
"example": "driver-alice"
},
"sdp": {
"type": "string"
},
"type": {
"type": "string",
"example": "offer"
}
}
},
"transport.WebRTCConnectResponse": {
"type": "object",
"properties": {
"sdp": {
"type": "string"
},
"type": {
"type": "string",
"example": "answer"
}
}
} }
} }
}` }`
+485 -6
View File
@@ -946,7 +946,7 @@
} }
} }
}, },
"/api/drivers/{id}/device": { "/api/drivers/{id}/car": {
"put": { "put": {
"description": "Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is a convenience that returns the driver by 3-letter nickname.", "description": "Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is a convenience that returns the driver by 3-letter nickname.",
"produces": [ "produces": [
@@ -1010,6 +1010,120 @@
} }
} }
}, },
"/api/leaderboard": {
"get": {
"description": "Returns paginated leaderboard list. Supports overall leaderboard or track-specific leaderboard for the current year.",
"produces": [
"application/json"
],
"tags": [
"leaderboard"
],
"summary": "Leaderboard",
"parameters": [
{
"type": "string",
"description": "Track ID. If empty, returns overall leaderboard.",
"name": "track_id",
"in": "query"
},
{
"type": "integer",
"description": "Year. Defaults to current year.",
"name": "year",
"in": "query"
},
{
"type": "integer",
"description": "Limit (1..200, default 50)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset (default 0)",
"name": "offset",
"in": "query"
},
{
"type": "string",
"description": "Current driver ID. Defaults to driver-seed-001 (mock Alice).",
"name": "X-Driver-Id",
"in": "header"
}
],
"responses": {
"200": {
"description": "Leaderboard page and current driver position",
"schema": {
"$ref": "#/definitions/transport.LeaderboardResponse"
}
},
"400": {
"description": "Invalid input",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/leaderboard/tracks": {
"get": {
"description": "Returns the list of scheduled tracks with their status (finished/active/planned), top 3 drivers, and current driver position, sorted by calendar start date.",
"produces": [
"application/json"
],
"tags": [
"leaderboard"
],
"summary": "Track Calendar Leaderboards",
"parameters": [
{
"type": "integer",
"description": "Year. Defaults to current year.",
"name": "year",
"in": "query"
},
{
"type": "string",
"description": "Current driver ID. Defaults to driver-seed-001 (mock Alice).",
"name": "X-Driver-Id",
"in": "header"
}
],
"responses": {
"200": {
"description": "Calendar leaderboards list",
"schema": {
"$ref": "#/definitions/transport.TrackCalendarLeaderboardResponse"
}
},
"400": {
"description": "Invalid input",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/races": { "/api/races": {
"get": { "get": {
"description": "Returns one keyset page of races. Live races (status lobby|racing) come from in-memory lobby.Service; finished races come from Postgres. The two sources are merged and ordered by (started_ms DESC, id ASC). The cursor is opaque: pass back the next_cursor verbatim from a previous page.\n## Filters\n- `status` (comma-separated, e.g. \"racing,finished\"). Recognised values: all, lobby, racing, finished.\n- `track_id` (exact match against a physical track id; custom lobbies are not supported in this build).\n- `limit` (1..200, default 50)", "description": "Returns one keyset page of races. Live races (status lobby|racing) come from in-memory lobby.Service; finished races come from Postgres. The two sources are merged and ordered by (started_ms DESC, id ASC). The cursor is opaque: pass back the next_cursor verbatim from a previous page.\n## Filters\n- `status` (comma-separated, e.g. \"racing,finished\"). Recognised values: all, lobby, racing, finished.\n- `track_id` (exact match against a physical track id; custom lobbies are not supported in this build).\n- `limit` (1..200, default 50)",
@@ -1518,6 +1632,123 @@
} }
} }
}, },
"/api/tracks/calendar": {
"get": {
"description": "Returns the full calendar of track active periods.",
"produces": [
"application/json"
],
"tags": [
"tracks"
],
"summary": "Get Track Calendar",
"responses": {
"200": {
"description": "List of scheduled active periods",
"schema": {
"$ref": "#/definitions/transport.TrackCalendarResponse"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"post": {
"description": "Schedules an active period for a physical track.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"tracks"
],
"summary": "Schedule Track Active Period",
"parameters": [
{
"description": "Calendar entry details",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/transport.TrackCalendarCreateRequest"
}
}
],
"responses": {
"201": {
"description": "Created calendar entry",
"schema": {
"$ref": "#/definitions/transport.TrackCalendarEntry"
}
},
"400": {
"description": "Invalid input",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Track not found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/tracks/calendar/{id}": {
"delete": {
"description": "Deletes a scheduled active period by ID.",
"tags": [
"tracks"
],
"summary": "Delete Scheduled Period",
"parameters": [
{
"type": "integer",
"description": "Calendar Entry ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/tracks/{id}": { "/api/tracks/{id}": {
"get": { "get": {
"description": "Dispatches on HTTP method. The id is taken from the URL path (`/api/tracks/{id}`). System tracks are immutable and cannot be updated or deleted.", "description": "Dispatches on HTTP method. The id is taken from the URL path (`/api/tracks/{id}`). System tracks are immutable and cannot be updated or deleted.",
@@ -1770,7 +2001,7 @@
}, },
"/api/video/control": { "/api/video/control": {
"post": { "post": {
"description": "Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888).", "description": "Sends a text command or a JSON control packet to the ESP32 command port (UDP 8888).",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -1787,10 +2018,20 @@
}, },
{ {
"type": "string", "type": "string",
"description": "Command text", "description": "Command text (legacy)",
"name": "cmd", "name": "cmd",
"in": "query", "in": "query"
"required": true },
{
"description": "Control packet JSON (steer, throttle, brake, gear, flags)",
"name": "body",
"in": "body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
} }
], ],
"responses": { "responses": {
@@ -1802,7 +2043,7 @@
} }
}, },
"400": { "400": {
"description": "Missing cmd parameter", "description": "Missing parameter or invalid body",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1844,6 +2085,40 @@
} }
} }
}, },
"/api/webrtc/connect": {
"post": {
"description": "Accepts SDP Offer, negotiates peer connection, and returns SDP Answer. Routes video and control data via DataChannels.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"webrtc"
],
"summary": "Establish WebRTC connection for driver-car 1-to-1 proxying",
"parameters": [
{
"description": "SDP Offer details",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/transport.WebRTCConnectRequest"
}
}
],
"responses": {
"200": {
"description": "SDP Answer negotiation success",
"schema": {
"$ref": "#/definitions/transport.WebRTCConnectResponse"
}
}
}
}
},
"/health": { "/health": {
"get": { "get": {
"description": "Returns 200 OK with current engine phase, WS connection count and total snapshot drops. Intended for load-balancer health checks.", "description": "Returns 200 OK with current engine phase, WS connection count and total snapshot drops. Intended for load-balancer health checks.",
@@ -1965,6 +2240,9 @@
"color_hex": { "color_hex": {
"type": "string" "type": "string"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2032,6 +2310,9 @@
"created_ms": { "created_ms": {
"type": "integer" "type": "integer"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2279,6 +2560,78 @@
} }
} }
}, },
"transport.LeaderboardEntry": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string",
"example": "https://cdn.example.com/u/ace.png"
},
"best_pos": {
"description": "Only populated for track-specific leaderboard",
"type": "integer",
"example": 1
},
"best_time_ms": {
"type": "integer",
"example": 15000
},
"clan_id": {
"type": "string",
"example": "clan-seed-001"
},
"clan_tag": {
"type": "string",
"example": "ACE"
},
"driver_id": {
"type": "string",
"example": "driver-seed-001"
},
"name": {
"type": "string",
"example": "Alice"
},
"nickname": {
"type": "string",
"example": "ACE"
},
"points": {
"type": "integer",
"example": 25
},
"rank": {
"type": "integer",
"example": 1
}
}
},
"transport.LeaderboardResponse": {
"type": "object",
"properties": {
"current_driver": {
"$ref": "#/definitions/transport.LeaderboardEntry"
},
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.LeaderboardEntry"
}
},
"limit": {
"type": "integer",
"example": 50
},
"offset": {
"type": "integer",
"example": 0
},
"total": {
"type": "integer",
"example": 8
}
}
},
"transport.LobbyRace": { "transport.LobbyRace": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2739,6 +3092,100 @@
} }
} }
}, },
"transport.TrackCalendarCreateRequest": {
"type": "object",
"properties": {
"end_at": {
"type": "string",
"example": "2026-07-30T00:00:00Z"
},
"start_at": {
"type": "string",
"example": "2026-07-15T00:00:00Z"
},
"track_id": {
"type": "string",
"example": "monaco"
}
}
},
"transport.TrackCalendarEntry": {
"type": "object",
"properties": {
"end_at": {
"type": "string",
"example": "2026-07-30T00:00:00Z"
},
"id": {
"type": "integer",
"example": 1
},
"start_at": {
"type": "string",
"example": "2026-07-15T00:00:00Z"
},
"track_id": {
"type": "string",
"example": "monaco"
}
}
},
"transport.TrackCalendarLeaderboard": {
"type": "object",
"properties": {
"current_driver": {
"$ref": "#/definitions/transport.LeaderboardEntry"
},
"end_at": {
"type": "string",
"example": "2026-07-30T00:00:00Z"
},
"podium": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.LeaderboardEntry"
}
},
"start_at": {
"type": "string",
"example": "2026-07-15T00:00:00Z"
},
"status": {
"type": "string",
"example": "active"
},
"track_id": {
"type": "string",
"example": "monaco"
},
"track_name": {
"type": "string",
"example": "Monaco"
}
}
},
"transport.TrackCalendarLeaderboardResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.TrackCalendarLeaderboard"
}
}
}
},
"transport.TrackCalendarResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/transport.TrackCalendarEntry"
}
}
}
},
"transport.TrackCreateRequest": { "transport.TrackCreateRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2849,6 +3296,10 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"is_active": {
"type": "boolean",
"example": true
},
"lane_width_m": { "lane_width_m": {
"type": "number" "type": "number"
}, },
@@ -2889,6 +3340,34 @@
"example": "0.1.0-poc" "example": "0.1.0-poc"
} }
} }
},
"transport.WebRTCConnectRequest": {
"type": "object",
"properties": {
"driver_id": {
"type": "string",
"example": "driver-alice"
},
"sdp": {
"type": "string"
},
"type": {
"type": "string",
"example": "offer"
}
}
},
"transport.WebRTCConnectResponse": {
"type": "object",
"properties": {
"sdp": {
"type": "string"
},
"type": {
"type": "string",
"example": "answer"
}
}
} }
} }
} }
+335 -6
View File
@@ -39,6 +39,8 @@ definitions:
$ref: '#/definitions/transport.ChassisWire' $ref: '#/definitions/transport.ChassisWire'
color_hex: color_hex:
type: string type: string
device_id:
type: integer
drive: drive:
type: string type: string
height_mm: height_mm:
@@ -83,6 +85,8 @@ definitions:
type: string type: string
created_ms: created_ms:
type: integer type: integer
device_id:
type: integer
drive: drive:
type: string type: string
height_mm: height_mm:
@@ -252,6 +256,58 @@ definitions:
type: type:
$ref: '#/definitions/transport.MessageType' $ref: '#/definitions/transport.MessageType'
type: object type: object
transport.LeaderboardEntry:
properties:
avatar_url:
example: https://cdn.example.com/u/ace.png
type: string
best_pos:
description: Only populated for track-specific leaderboard
example: 1
type: integer
best_time_ms:
example: 15000
type: integer
clan_id:
example: clan-seed-001
type: string
clan_tag:
example: ACE
type: string
driver_id:
example: driver-seed-001
type: string
name:
example: Alice
type: string
nickname:
example: ACE
type: string
points:
example: 25
type: integer
rank:
example: 1
type: integer
type: object
transport.LeaderboardResponse:
properties:
current_driver:
$ref: '#/definitions/transport.LeaderboardEntry'
items:
items:
$ref: '#/definitions/transport.LeaderboardEntry'
type: array
limit:
example: 50
type: integer
offset:
example: 0
type: integer
total:
example: 8
type: integer
type: object
transport.LobbyRace: transport.LobbyRace:
properties: properties:
created_ms: created_ms:
@@ -592,6 +648,71 @@ definitions:
width_m: width_m:
type: number type: number
type: object type: object
transport.TrackCalendarCreateRequest:
properties:
end_at:
example: "2026-07-30T00:00:00Z"
type: string
start_at:
example: "2026-07-15T00:00:00Z"
type: string
track_id:
example: monaco
type: string
type: object
transport.TrackCalendarEntry:
properties:
end_at:
example: "2026-07-30T00:00:00Z"
type: string
id:
example: 1
type: integer
start_at:
example: "2026-07-15T00:00:00Z"
type: string
track_id:
example: monaco
type: string
type: object
transport.TrackCalendarLeaderboard:
properties:
current_driver:
$ref: '#/definitions/transport.LeaderboardEntry'
end_at:
example: "2026-07-30T00:00:00Z"
type: string
podium:
items:
$ref: '#/definitions/transport.LeaderboardEntry'
type: array
start_at:
example: "2026-07-15T00:00:00Z"
type: string
status:
example: active
type: string
track_id:
example: monaco
type: string
track_name:
example: Monaco
type: string
type: object
transport.TrackCalendarLeaderboardResponse:
properties:
items:
items:
$ref: '#/definitions/transport.TrackCalendarLeaderboard'
type: array
type: object
transport.TrackCalendarResponse:
properties:
items:
items:
$ref: '#/definitions/transport.TrackCalendarEntry'
type: array
type: object
transport.TrackCreateRequest: transport.TrackCreateRequest:
properties: properties:
track: track:
@@ -664,6 +785,9 @@ definitions:
type: string type: string
id: id:
type: string type: string
is_active:
example: true
type: boolean
lane_width_m: lane_width_m:
type: number type: number
length_m: length_m:
@@ -691,6 +815,25 @@ definitions:
example: 0.1.0-poc example: 0.1.0-poc
type: string type: string
type: object type: object
transport.WebRTCConnectRequest:
properties:
driver_id:
example: driver-alice
type: string
sdp:
type: string
type:
example: offer
type: string
type: object
transport.WebRTCConnectResponse:
properties:
sdp:
type: string
type:
example: answer
type: string
type: object
host: localhost:8080 host: localhost:8080
info: info:
contact: contact:
@@ -1345,7 +1488,7 @@ paths:
summary: Get / update / delete a driver by id summary: Get / update / delete a driver by id
tags: tags:
- drivers - drivers
/api/drivers/{id}/device: /api/drivers/{id}/car:
put: put:
description: Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is description: Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is
a convenience that returns the driver by 3-letter nickname. a convenience that returns the driver by 3-letter nickname.
@@ -1389,6 +1532,84 @@ paths:
summary: Get / update / delete a driver by id summary: Get / update / delete a driver by id
tags: tags:
- drivers - drivers
/api/leaderboard:
get:
description: Returns paginated leaderboard list. Supports overall leaderboard
or track-specific leaderboard for the current year.
parameters:
- description: Track ID. If empty, returns overall leaderboard.
in: query
name: track_id
type: string
- description: Year. Defaults to current year.
in: query
name: year
type: integer
- description: Limit (1..200, default 50)
in: query
name: limit
type: integer
- description: Offset (default 0)
in: query
name: offset
type: integer
- description: Current driver ID. Defaults to driver-seed-001 (mock Alice).
in: header
name: X-Driver-Id
type: string
produces:
- application/json
responses:
"200":
description: Leaderboard page and current driver position
schema:
$ref: '#/definitions/transport.LeaderboardResponse'
"400":
description: Invalid input
schema:
additionalProperties: true
type: object
"500":
description: Internal server error
schema:
additionalProperties: true
type: object
summary: Leaderboard
tags:
- leaderboard
/api/leaderboard/tracks:
get:
description: Returns the list of scheduled tracks with their status (finished/active/planned),
top 3 drivers, and current driver position, sorted by calendar start date.
parameters:
- description: Year. Defaults to current year.
in: query
name: year
type: integer
- description: Current driver ID. Defaults to driver-seed-001 (mock Alice).
in: header
name: X-Driver-Id
type: string
produces:
- application/json
responses:
"200":
description: Calendar leaderboards list
schema:
$ref: '#/definitions/transport.TrackCalendarLeaderboardResponse'
"400":
description: Invalid input
schema:
additionalProperties: true
type: object
"500":
description: Internal server error
schema:
additionalProperties: true
type: object
summary: Track Calendar Leaderboards
tags:
- leaderboard
/api/races: /api/races:
get: get:
description: |- description: |-
@@ -1908,6 +2129,85 @@ paths:
summary: Get / update / delete a track by id summary: Get / update / delete a track by id
tags: tags:
- tracks - tracks
/api/tracks/calendar:
get:
description: Returns the full calendar of track active periods.
produces:
- application/json
responses:
"200":
description: List of scheduled active periods
schema:
$ref: '#/definitions/transport.TrackCalendarResponse'
"500":
description: Internal error
schema:
additionalProperties: true
type: object
summary: Get Track Calendar
tags:
- tracks
post:
consumes:
- application/json
description: Schedules an active period for a physical track.
parameters:
- description: Calendar entry details
in: body
name: payload
required: true
schema:
$ref: '#/definitions/transport.TrackCalendarCreateRequest'
produces:
- application/json
responses:
"201":
description: Created calendar entry
schema:
$ref: '#/definitions/transport.TrackCalendarEntry'
"400":
description: Invalid input
schema:
additionalProperties: true
type: object
"404":
description: Track not found
schema:
additionalProperties: true
type: object
"500":
description: Internal error
schema:
additionalProperties: true
type: object
summary: Schedule Track Active Period
tags:
- tracks
/api/tracks/calendar/{id}:
delete:
description: Deletes a scheduled active period by ID.
parameters:
- description: Calendar Entry ID
in: path
name: id
required: true
type: integer
responses:
"204":
description: No Content
"404":
description: Not found
schema:
additionalProperties: true
type: object
"500":
description: Internal error
schema:
additionalProperties: true
type: object
summary: Delete Scheduled Period
tags:
- tracks
/api/version: /api/version:
get: get:
description: Returns the current server version. description: Returns the current server version.
@@ -1923,18 +2223,24 @@ paths:
- system - system
/api/video/control: /api/video/control:
post: post:
description: Sends a text command (e.g. start, stop, left, right) to the ESP32 description: Sends a text command or a JSON control packet to the ESP32 command
command port (UDP 8888). port (UDP 8888).
parameters: parameters:
- description: Device ID (0..127) - description: Device ID (0..127)
in: query in: query
name: id name: id
type: integer type: integer
- description: Command text - description: Command text (legacy)
in: query in: query
name: cmd name: cmd
required: true
type: string type: string
- description: Control packet JSON (steer, throttle, brake, gear, flags)
in: body
name: body
schema:
additionalProperties:
type: integer
type: object
produces: produces:
- application/json - application/json
responses: responses:
@@ -1944,7 +2250,7 @@ paths:
additionalProperties: true additionalProperties: true
type: object type: object
"400": "400":
description: Missing cmd parameter description: Missing parameter or invalid body
schema: schema:
type: string type: string
"500": "500":
@@ -1973,6 +2279,29 @@ paths:
summary: Get MJPEG video stream from ESP32 summary: Get MJPEG video stream from ESP32
tags: tags:
- video - video
/api/webrtc/connect:
post:
consumes:
- application/json
description: Accepts SDP Offer, negotiates peer connection, and returns SDP
Answer. Routes video and control data via DataChannels.
parameters:
- description: SDP Offer details
in: body
name: body
required: true
schema:
$ref: '#/definitions/transport.WebRTCConnectRequest'
produces:
- application/json
responses:
"200":
description: SDP Answer negotiation success
schema:
$ref: '#/definitions/transport.WebRTCConnectResponse'
summary: Establish WebRTC connection for driver-car 1-to-1 proxying
tags:
- webrtc
/health: /health:
get: get:
description: Returns 200 OK with current engine phase, WS connection count and description: Returns 200 OK with current engine phase, WS connection count and
+24 -4
View File
@@ -20,12 +20,32 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/mailru/easyjson v0.7.6 // indirect
github.com/pion/datachannel v1.6.2 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/pion/ice/v4 v4.2.7 // indirect
github.com/pion/interceptor v0.1.45 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.16 // indirect
github.com/pion/rtp v1.10.2 // indirect
github.com/pion/sctp v1.10.3 // indirect
github.com/pion/sdp/v3 v3.0.19 // indirect
github.com/pion/srtp/v3 v3.0.12 // indirect
github.com/pion/stun/v3 v3.1.6 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect
github.com/pion/turn/v5 v5.0.10 // indirect
github.com/pion/webrtc/v4 v4.2.16 // indirect
github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/files v1.0.1 // indirect
github.com/swaggo/http-swagger v1.3.4 // indirect github.com/swaggo/http-swagger v1.3.4 // indirect
github.com/swaggo/swag v1.16.4 // indirect github.com/swaggo/swag v1.16.4 // indirect
golang.org/x/net v0.43.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/tools v0.36.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
+48
View File
@@ -39,6 +39,38 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pion/datachannel v1.6.2 h1:7EXQ8TH3vTouBUdRWYbcX2edSx9Yj6k5zl5P+qyxEPc=
github.com/pion/datachannel v1.6.2/go.mod h1:pzbdAZvyGtXbcHM1hBbsFaOTf40lZizU/dNlvVOak6E=
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
github.com/pion/ice/v4 v4.2.7 h1:zDEbC6MiEdhQpF8TxBOTws+NU6ZgGpveHrQq4Lc1kao=
github.com/pion/ice/v4 v4.2.7/go.mod h1:9SNPaq0c7El/ki8leJzyCkK10zsskprR3zTNbO3monY=
github.com/pion/interceptor v0.1.45 h1:6PUo/5829bIfRFIPPJQzuDn8EjxRTSB/CSD7QVCOaqo=
github.com/pion/interceptor v0.1.45/go.mod h1:gNDYM/uFKcLe/B3gS2/7+aw6z+RDiMy2qKTnF1LO31w=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo=
github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
github.com/pion/sctp v1.10.3 h1:1gBtLMA9lmwNuJkZSZJCdD5/Hz4yJs+7dAqi6ZY97QI=
github.com/pion/sctp v1.10.3/go.mod h1:7KFmTwLcoYgJs/Z+99nJvsWL0qDpuyloSI0RbAqlrz0=
github.com/pion/sdp/v3 v3.0.19 h1:1VMKs3gIkTQV5M3hNKfTAPrDXSNrYtOlmOD8+mSZUGQ=
github.com/pion/sdp/v3 v3.0.19/go.mod h1:dE5WOSlzXrtiE/iuZqe9n+AcEbOjtAd3k5m5NtlV/qU=
github.com/pion/srtp/v3 v3.0.12 h1:U7V17bckl7sI4mb3sepiojByDuBY0wNCqQE+6IlQBbc=
github.com/pion/srtp/v3 v3.0.12/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns=
github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pion/turn/v5 v5.0.10 h1:mOMZjudflXpte5OsCnXztpUKwNXcpXIAzMBnq9TXOSQ=
github.com/pion/turn/v5 v5.0.10/go.mod h1:u3XjBqy2Z4+NhCUpDoOSsNuQDrPLvKStlCGWk6sTQ1E=
github.com/pion/webrtc/v4 v4.2.16 h1:oK1GAg0TWJtZWYB8J/BgTgGWPoV2148gQWocH12vr3Q=
github.com/pion/webrtc/v4 v4.2.16/go.mod h1:y4HjLAkX90LH+C/qPqGOUgz8RA8CbDj3Iar3d+2hdKQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -53,9 +85,13 @@ github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -63,16 +99,22 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -82,11 +124,17 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+51
View File
@@ -247,6 +247,7 @@ type CreateCarOptions struct {
ColorHex string ColorHex string
AvatarURL string AvatarURL string
Active *bool // optional; defaults to true Active *bool // optional; defaults to true
DeviceID *int
} }
// Validate sanity-checks dimensions and required fields. // Validate sanity-checks dimensions and required fields.
@@ -319,6 +320,7 @@ func (s *Service) CreateCar(ctx context.Context, opts CreateCarOptions) (CarMeta
ColorHex: opts.ColorHex, ColorHex: opts.ColorHex,
AvatarURL: opts.AvatarURL, AvatarURL: opts.AvatarURL,
Active: active, Active: active,
DeviceID: opts.DeviceID,
CreatedMs: now, CreatedMs: now,
UpdatedMs: now, UpdatedMs: now,
} }
@@ -345,6 +347,7 @@ type UpdateCarOptions struct {
Motor *MotorSpec Motor *MotorSpec
Battery *BatterySpec Battery *BatterySpec
Drive *Drivetrain Drive *Drivetrain
DeviceID *int // double pointer or *int to clear / set? Let's use *int, but to allow setting to nil (e.g. if we pass -1 or use *int), wait, actually, we can support DeviceID **int or *int. Let's use *int for now or wait, if *int is nil, it means do not update. If they want to set to a device ID, they pass a pointer. Wait! What if they want to clear it? They can pass a pointer to a negative value like -1 or we can use a double pointer. Actually, in Go, *int is very simple. Let's make it *int. If we need to clear, we can pass a device ID of -1 and map it to nil. Let's check how the JSON schema handles it. Usually *int is sufficient. Wait, let's use *int.
} }
// UpdateCar applies a partial update. // UpdateCar applies a partial update.
@@ -403,6 +406,13 @@ func (s *Service) UpdateCar(ctx context.Context, id string, opts UpdateCarOption
if opts.Drive != nil { if opts.Drive != nil {
c.Drive = *opts.Drive c.Drive = *opts.Drive
} }
if opts.DeviceID != nil {
if *opts.DeviceID < 0 {
c.DeviceID = nil
} else {
c.DeviceID = opts.DeviceID
}
}
c.UpdatedMs = time.Now().UnixMilli() c.UpdatedMs = time.Now().UnixMilli()
if c.LengthMm < minCarLengthMm || c.LengthMm > maxCarLengthMm || if c.LengthMm < minCarLengthMm || c.LengthMm > maxCarLengthMm ||
@@ -607,3 +617,44 @@ func (s *Service) ListCarsByOwner(ownerID string) []CarMeta {
} }
return out return out
} }
// GetTrackCalendar returns all track calendar entries from the store.
func (s *Service) GetTrackCalendar(ctx context.Context) ([]TrackCalendarEntry, error) {
if s.store == nil {
return nil, ErrStoreMissing
}
return s.store.GetTrackCalendar(ctx)
}
// CreateTrackCalendar creates a new calendar entry.
func (s *Service) CreateTrackCalendar(ctx context.Context, trackID string, startAt, endAt time.Time) (TrackCalendarEntry, error) {
if s.store == nil {
return TrackCalendarEntry{}, ErrStoreMissing
}
s.mu.RLock()
_, trackExists := s.tracks[TrackID(trackID)]
s.mu.RUnlock()
if !trackExists {
return TrackCalendarEntry{}, fmt.Errorf("%w: track %s does not exist", ErrNotFound, trackID)
}
if startAt.After(endAt) || startAt.Equal(endAt) {
return TrackCalendarEntry{}, fmt.Errorf("%w: start time must be before end time", ErrInvalidInput)
}
entry := TrackCalendarEntry{
TrackID: trackID,
StartAt: startAt,
EndAt: endAt,
}
return s.store.CreateTrackCalendar(ctx, entry)
}
// DeleteTrackCalendar deletes a calendar entry.
func (s *Service) DeleteTrackCalendar(ctx context.Context, id int) error {
if s.store == nil {
return ErrStoreMissing
}
return s.store.DeleteTrackCalendar(ctx, id)
}
+50
View File
@@ -399,3 +399,53 @@ func TestServiceWithoutStore(t *testing.T) {
t.Fatal("expected error when store is nil") t.Fatal("expected error when store is nil")
} }
} }
func TestTrackCalendar(t *testing.T) {
s, _ := loadSeeds()
defer s.Stop()
ctx := context.Background()
// 1. Initially calendar should be empty
entries, err := s.GetTrackCalendar(ctx)
if err != nil {
t.Fatalf("get calendar: %v", err)
}
if len(entries) != 0 {
t.Errorf("expected 0 entries, got %d", len(entries))
}
// 2. Schedule a track
start := time.Now().Add(-1 * time.Hour)
end := time.Now().Add(1 * time.Hour)
created, err := s.CreateTrackCalendar(ctx, "monaco", start, end)
if err != nil {
t.Fatalf("create calendar: %v", err)
}
if created.TrackID != "monaco" || !created.StartAt.Equal(start) || !created.EndAt.Equal(end) {
t.Errorf("invalid created entry: %+v", created)
}
// 3. Get calendar should return the entry
entries, err = s.GetTrackCalendar(ctx)
if err != nil {
t.Fatalf("get calendar: %v", err)
}
if len(entries) != 1 || entries[0].ID != created.ID {
t.Errorf("expected 1 entry with id %d, got %v", created.ID, entries)
}
// 4. Delete entry
err = s.DeleteTrackCalendar(ctx, created.ID)
if err != nil {
t.Fatalf("delete calendar: %v", err)
}
// 5. Get calendar should be empty again
entries, err = s.GetTrackCalendar(ctx)
if err != nil {
t.Fatalf("get calendar: %v", err)
}
if len(entries) != 0 {
t.Errorf("expected 0 entries, got %d", len(entries))
}
}
+11 -1
View File
@@ -17,13 +17,23 @@ import "math"
// (1/24 scale), power figures are real (full-scale 1.6L V6 hybrid // (1/24 scale), power figures are real (full-scale 1.6L V6 hybrid
// turbo, ~1000 hp combined MGU-K + ICE). // turbo, ~1000 hp combined MGU-K + ICE).
func DefaultTracks() []Track { func DefaultTracks() []Track {
return []Track{ tracks := []Track{
Monaco(), Monaco(),
BarcelonaCatalunya(), BarcelonaCatalunya(),
AustriaRedBullRing(), AustriaRedBullRing(),
GreatBritainSilverstone(), GreatBritainSilverstone(),
BelgiumSpa(), BelgiumSpa(),
} }
for i := range tracks {
for j := range tracks[i].Centerline {
tracks[i].Centerline[j].Y = -tracks[i].Centerline[j].Y
tracks[i].Centerline[j].HeadingRad = -tracks[i].Centerline[j].HeadingRad
}
tracks[i].Bounds = ComputeBounds(tracks[i].Centerline)
tracks[i].LengthM = tracks[i].Bounds.LengthM
tracks[i].WidthM = tracks[i].Bounds.WidthM
}
return tracks
} }
func DefaultCars() []Car { func DefaultCars() []Car {
+5
View File
@@ -27,4 +27,9 @@ type Store interface {
// UpdateCarStats applies a partial mutation of read-mostly stats // UpdateCarStats applies a partial mutation of read-mostly stats
// fields on a car. Used by the race engine hooks. // fields on a car. Used by the race engine hooks.
UpdateCarStats(ctx context.Context, id string, fn func(*CarMeta)) error UpdateCarStats(ctx context.Context, id string, fn func(*CarMeta)) error
// Track Calendar methods
GetTrackCalendar(ctx context.Context) ([]TrackCalendarEntry, error)
CreateTrackCalendar(ctx context.Context, entry TrackCalendarEntry) (TrackCalendarEntry, error)
DeleteTrackCalendar(ctx context.Context, id int) error
} }
+31
View File
@@ -11,12 +11,15 @@ type memoryStore struct {
mu sync.Mutex mu sync.Mutex
tracks map[TrackID]TrackMeta tracks map[TrackID]TrackMeta
cars map[CarID]CarMeta cars map[CarID]CarMeta
calendar []TrackCalendarEntry
nextID int
} }
func newMemoryStore() *memoryStore { func newMemoryStore() *memoryStore {
return &memoryStore{ return &memoryStore{
tracks: make(map[TrackID]TrackMeta), tracks: make(map[TrackID]TrackMeta),
cars: make(map[CarID]CarMeta), cars: make(map[CarID]CarMeta),
nextID: 1,
} }
} }
@@ -73,3 +76,31 @@ func (m *memoryStore) UpdateCarStats(_ context.Context, id string, fn func(*CarM
m.cars[id] = c m.cars[id] = c
return nil return nil
} }
func (m *memoryStore) GetTrackCalendar(_ context.Context) ([]TrackCalendarEntry, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := append([]TrackCalendarEntry(nil), m.calendar...)
return out, nil
}
func (m *memoryStore) CreateTrackCalendar(_ context.Context, entry TrackCalendarEntry) (TrackCalendarEntry, error) {
m.mu.Lock()
defer m.mu.Unlock()
entry.ID = m.nextID
m.nextID++
m.calendar = append(m.calendar, entry)
return entry, nil
}
func (m *memoryStore) DeleteTrackCalendar(_ context.Context, id int) error {
m.mu.Lock()
defer m.mu.Unlock()
for i, entry := range m.calendar {
if entry.ID == id {
m.calendar = append(m.calendar[:i], m.calendar[i+1:]...)
return nil
}
}
return ErrNotFound
}
+9
View File
@@ -94,6 +94,14 @@ type TrackMeta struct {
UpdatedMs int64 `json:"updated_ms"` UpdatedMs int64 `json:"updated_ms"`
} }
// TrackCalendarEntry describes one active period for a track.
type TrackCalendarEntry struct {
ID int `json:"id"`
TrackID string `json:"track_id"`
StartAt time.Time `json:"start_at"`
EndAt time.Time `json:"end_at"`
}
// MotorSpec describes the electric motor. // MotorSpec describes the electric motor.
type MotorSpec struct { type MotorSpec struct {
Kind string `json:"kind"` // brushed | brushless Kind string `json:"kind"` // brushed | brushless
@@ -138,6 +146,7 @@ type CarMeta struct {
Motor MotorSpec `json:"motor"` Motor MotorSpec `json:"motor"`
Battery BatterySpec `json:"battery"` Battery BatterySpec `json:"battery"`
Drive Drivetrain `json:"drive"` Drive Drivetrain `json:"drive"`
DeviceID *int `json:"device_id,omitempty"` // mapped physical ESP32 device ID
// Performance envelope. // Performance envelope.
TopSpeedMs float64 `json:"top_speed_ms"` TopSpeedMs float64 `json:"top_speed_ms"`
+3 -1
View File
@@ -47,6 +47,7 @@ type Car struct {
ID string ID string
DriverID string // session id DriverID string // session id
DriverName string DriverName string
DeviceID *int // internal mapping to physical ESP32 device ID
X, Y float64 X, Y float64
Heading float64 // radians Heading float64 // radians
Speed float64 // m/s Speed float64 // m/s
@@ -169,7 +170,7 @@ func (e *Engine) SetPhase(p RacePhase) { e.phase.Store(int32(p)) }
// AddCar registers a car and returns it. Returns an error if the driver // AddCar registers a car and returns it. Returns an error if the driver
// already has a car or the max car count is reached. // already has a car or the max car count is reached.
func (e *Engine) AddCar(driverID, driverName string, slot int) (*Car, error) { func (e *Engine) AddCar(driverID, driverName string, slot int, deviceID *int) (*Car, error) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
@@ -185,6 +186,7 @@ func (e *Engine) AddCar(driverID, driverName string, slot int) (*Car, error) {
ID: id, ID: id,
DriverID: driverID, DriverID: driverID,
DriverName: driverName, DriverName: driverName,
DeviceID: deviceID,
// Spread cars along start grid (x = 0, y = slot * 0.4 m). // Spread cars along start grid (x = 0, y = slot * 0.4 m).
Y: float64(slot) * 0.4, Y: float64(slot) * 0.4,
} }
+14 -20
View File
@@ -187,7 +187,7 @@ func (s *Service) RemoveDriver(id string) {
d.Status = DriverStatusOffline d.Status = DriverStatusOffline
d.LastSeenMs = time.Now().UnixMilli() d.LastSeenMs = time.Now().UnixMilli()
d.RaceID = "" d.RaceID = ""
d.DeviceID = nil d.CarID = nil
hook := s.onRaceRemoved hook := s.onRaceRemoved
s.mu.Unlock() s.mu.Unlock()
@@ -244,9 +244,9 @@ func (s *Service) AddDriverToRace(driverID, raceID string) error {
s.mu.Unlock() s.mu.Unlock()
return ErrDriverNotFound return ErrDriverNotFound
} }
if d.DeviceID == nil { if d.CarID == nil || *d.CarID == "" {
s.mu.Unlock() s.mu.Unlock()
return fmt.Errorf("driver %s has no device selected", driverID) return fmt.Errorf("driver %s has no car selected", driverID)
} }
r, ok := s.races[raceID] r, ok := s.races[raceID]
if !ok { if !ok {
@@ -306,7 +306,7 @@ func (s *Service) RemoveDriverFromRace(driverID string) {
} }
d.Status = DriverStatusIdle d.Status = DriverStatusIdle
d.RaceID = "" d.RaceID = ""
d.DeviceID = nil d.CarID = nil
prevRaceID := d.RaceID prevRaceID := d.RaceID
hook := s.onRaceRemoved hook := s.onRaceRemoved
s.mu.Unlock() s.mu.Unlock()
@@ -349,7 +349,7 @@ func (s *Service) SetRaceStatus(raceID string, status RaceStatus) error {
if d, ok := s.drivers[did]; ok { if d, ok := s.drivers[did]; ok {
d.Status = DriverStatusIdle d.Status = DriverStatusIdle
d.RaceID = "" d.RaceID = ""
d.DeviceID = nil d.CarID = nil
affected = append(affected, *d) affected = append(affected, *d)
} }
} }
@@ -454,16 +454,10 @@ func (s *Service) GetDriver(driverID string) (DriverMeta, error) {
return *d, nil return *d, nil
} }
// SelectDevice assigns a physical device ID (0..127) to a driver. // SelectCar assigns a catalog car ID to a driver.
// Pass deviceID = nil to release the device. // If carID is nil, it clears the driver's car assignment.
// Returns an error if the device is already taken by another online driver. // It returns an error if the car is already taken by another active driver.
func (s *Service) SelectDevice(driverID string, deviceID *int) error { func (s *Service) SelectCar(driverID string, carID *string) error {
if deviceID != nil {
if *deviceID < 0 || *deviceID > 127 {
return fmt.Errorf("invalid device ID: must be 0..127")
}
}
s.mu.Lock() s.mu.Lock()
d, ok := s.drivers[driverID] d, ok := s.drivers[driverID]
if !ok { if !ok {
@@ -471,17 +465,17 @@ func (s *Service) SelectDevice(driverID string, deviceID *int) error {
return ErrDriverNotFound return ErrDriverNotFound
} }
// Enforce that device cannot be selected if already assigned to another driver. // Enforce that car cannot be selected if already assigned to another driver.
if deviceID != nil { if carID != nil && *carID != "" {
for otherID, otherD := range s.drivers { for otherID, otherD := range s.drivers {
if otherID != driverID && otherD.Status != DriverStatusOffline && otherD.DeviceID != nil && *otherD.DeviceID == *deviceID { if otherID != driverID && otherD.Status != DriverStatusOffline && otherD.CarID != nil && *otherD.CarID == *carID {
s.mu.Unlock() s.mu.Unlock()
return fmt.Errorf("device %d is already taken by driver %s", *deviceID, otherID) return fmt.Errorf("car %s is already taken by driver %s", *carID, otherID)
} }
} }
} }
d.DeviceID = deviceID d.CarID = carID
out := *d out := *d
s.mu.Unlock() s.mu.Unlock()
+3 -3
View File
@@ -75,9 +75,9 @@ func TestQuickPlayJoinsFirstRace(t *testing.T) {
s := NewService() s := NewService()
_, _ = s.AddDriver("d1", "Alice", "") _, _ = s.AddDriver("d1", "Alice", "")
_, _ = s.AddDriver("d2", "Bob", "") _, _ = s.AddDriver("d2", "Bob", "")
dev1, dev2 := 1, 2 car1, car2 := "car-1", "car-2"
_ = s.SelectDevice("d1", &dev1) _ = s.SelectCar("d1", &car1)
_ = s.SelectDevice("d2", &dev2) _ = s.SelectCar("d2", &car2)
r1, _ := s.QuickPlay("d1", 4) r1, _ := s.QuickPlay("d1", 4)
r2, err := s.QuickPlay("d2", 4) r2, err := s.QuickPlay("d2", 4)
if err != nil { if err != nil {
+1 -1
View File
@@ -65,7 +65,7 @@ type DriverMeta struct {
LastSeenMs int64 `json:"last_seen_ms"` LastSeenMs int64 `json:"last_seen_ms"`
Country string `json:"country,omitempty"` // optional, for UI Country string `json:"country,omitempty"` // optional, for UI
AvatarHue int `json:"avatar_hue,omitempty"` // 0..360, deterministic per id AvatarHue int `json:"avatar_hue,omitempty"` // 0..360, deterministic per id
DeviceID *int `json:"device_id,omitempty"` CarID *string `json:"car_id,omitempty"`
} }
// Snapshot is the immutable view broadcast to clients. // Snapshot is the immutable view broadcast to clients.
+497
View File
@@ -0,0 +1,497 @@
package races
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
// LeaderboardEntry is a single row in the leaderboard.
type LeaderboardEntry struct {
DriverID string
Name string
Nickname string
AvatarURL string
ClanID string
ClanTag string
Points int
BestPos *int // deprecated/optional
BestTimeMs *int64
Rank int
}
// LeaderboardResult groups the paginated items, total count, and current driver.
type LeaderboardResult struct {
Items []LeaderboardEntry
Total int
CurrentDriver *LeaderboardEntry
}
func getYearTimestamps(year int) (int64, int64) {
start := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(year+1, time.January, 1, 0, 0, 0, 0, time.UTC)
return start.UnixMilli(), end.UnixMilli()
}
// GetTrackLeaderboard returns track-specific leaderboard entries for the given track and year.
func (s *PgStore) GetTrackLeaderboard(ctx context.Context, trackID string, year int, limit, offset int) ([]LeaderboardEntry, int, error) {
startMs, endMs := getYearTimestamps(year)
if limit <= 0 {
limit = 50
}
countQuery := `
WITH best_results AS (
SELECT
rd.driver_id
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.track_id = $1
AND r.finished_ms >= $2
AND r.finished_ms < $3
GROUP BY rd.driver_id
)
SELECT COUNT(*) FROM best_results`
var total int
err := s.pool.QueryRow(ctx, countQuery, trackID, startMs, endMs).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("count track leaderboard: %w", err)
}
if total == 0 {
return nil, 0, nil
}
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
ranked_leaderboard AS (
SELECT
tp.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
tp.points,
tp.best_time,
ROW_NUMBER() OVER (ORDER BY tp.points DESC, tp.best_time ASC NULLS LAST, tp.driver_id ASC) as rank
FROM track_points tp
JOIN drivers d ON tp.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
WHERE tp.track_id = $3
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points, best_time
FROM ranked_leaderboard
ORDER BY rank ASC
LIMIT $4 OFFSET $5`
rows, err := s.pool.Query(ctx, query, startMs, endMs, trackID, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("query track leaderboard: %w", err)
}
defer rows.Close()
var entries []LeaderboardEntry
for rows.Next() {
var entry LeaderboardEntry
if err := rows.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points, &entry.BestTimeMs,
); err != nil {
return nil, 0, err
}
entries = append(entries, entry)
}
return entries, total, nil
}
// GetTrackLeaderboardDriver returns a single driver's rank on a track.
func (s *PgStore) GetTrackLeaderboardDriver(ctx context.Context, trackID string, year int, driverID string) (*LeaderboardEntry, error) {
startMs, endMs := getYearTimestamps(year)
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
ranked_leaderboard AS (
SELECT
tp.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
tp.points,
tp.best_time,
ROW_NUMBER() OVER (ORDER BY tp.points DESC, tp.best_time ASC NULLS LAST, tp.driver_id ASC) as rank
FROM track_points tp
JOIN drivers d ON tp.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
WHERE tp.track_id = $3
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points, best_time
FROM ranked_leaderboard
WHERE driver_id = $4`
row := s.pool.QueryRow(ctx, query, startMs, endMs, trackID, driverID)
var entry LeaderboardEntry
if err := row.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points, &entry.BestTimeMs,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &entry, nil
}
// GetOverallLeaderboard returns overall leaderboard entries for the given year.
func (s *PgStore) GetOverallLeaderboard(ctx context.Context, year int, limit, offset int) ([]LeaderboardEntry, int, error) {
startMs, endMs := getYearTimestamps(year)
if limit <= 0 {
limit = 50
}
countQuery := `
WITH best_results AS (
SELECT
rd.driver_id
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY rd.driver_id
)
SELECT COUNT(*) FROM best_results`
var total int
err := s.pool.QueryRow(ctx, countQuery, startMs, endMs).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("count overall leaderboard: %w", err)
}
if total == 0 {
return nil, 0, nil
}
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
overall_points AS (
SELECT
driver_id,
SUM(points) as points
FROM track_points
GROUP BY driver_id
),
ranked_overall AS (
SELECT
op.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
op.points,
ROW_NUMBER() OVER (ORDER BY op.points DESC, op.driver_id ASC) as rank
FROM overall_points op
JOIN drivers d ON op.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points
FROM ranked_overall
ORDER BY rank ASC
LIMIT $3 OFFSET $4`
rows, err := s.pool.Query(ctx, query, startMs, endMs, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("query overall leaderboard: %w", err)
}
defer rows.Close()
var entries []LeaderboardEntry
for rows.Next() {
var entry LeaderboardEntry
if err := rows.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points,
); err != nil {
return nil, 0, err
}
entries = append(entries, entry)
}
return entries, total, nil
}
// GetOverallLeaderboardDriver returns a single driver's overall rank.
func (s *PgStore) GetOverallLeaderboardDriver(ctx context.Context, year int, driverID string) (*LeaderboardEntry, error) {
startMs, endMs := getYearTimestamps(year)
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
overall_points AS (
SELECT
driver_id,
SUM(points) as points
FROM track_points
GROUP BY driver_id
),
ranked_overall AS (
SELECT
op.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
op.points,
ROW_NUMBER() OVER (ORDER BY op.points DESC, op.driver_id ASC) as rank
FROM overall_points op
JOIN drivers d ON op.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points
FROM ranked_overall
WHERE driver_id = $3`
row := s.pool.QueryRow(ctx, query, startMs, endMs, driverID)
var entry LeaderboardEntry
if err := row.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &entry, nil
}
// GetDriverLeaderboardProfile returns driver profile information.
func (s *PgStore) GetDriverLeaderboardProfile(ctx context.Context, driverID string) (*LeaderboardEntry, error) {
query := `
SELECT d.id, d.name, d.nickname, d.avatar_url, COALESCE(d.clan_id, ''), COALESCE(c.tag, '')
FROM drivers d
LEFT JOIN clans c ON d.clan_id = c.id
WHERE d.id = $1`
row := s.pool.QueryRow(ctx, query, driverID)
var entry LeaderboardEntry
if err := row.Scan(
&entry.DriverID, &entry.Name, &entry.Nickname, &entry.AvatarURL, &entry.ClanID, &entry.ClanTag,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &entry, nil
}
// TrackCalendarInfo holds the basic details of a scheduled track.
type TrackCalendarInfo struct {
ID int
TrackID string
TrackName string
StartAt time.Time
EndAt time.Time
}
// TrackCalendarLeaderboard holds the leaderboard summary for a track calendar event.
type TrackCalendarLeaderboard struct {
TrackID string
TrackName string
StartAt time.Time
EndAt time.Time
Status string // finished, active, planned
Podium []LeaderboardEntry
CurrentDriver *LeaderboardEntry
}
// GetCalendarLeaderboardTracks returns all track calendar entries joined with their track names, sorted chronologically.
func (s *PgStore) GetCalendarLeaderboardTracks(ctx context.Context) ([]TrackCalendarInfo, error) {
rows, err := s.pool.Query(ctx, `
SELECT tc.id, tc.track_id, t.name, tc.start_at, tc.end_at
FROM track_calendar tc
JOIN tracks t ON tc.track_id = t.id
ORDER BY tc.start_at DESC
`)
if err != nil {
return nil, fmt.Errorf("query calendar tracks: %w", err)
}
defer rows.Close()
var out []TrackCalendarInfo
for rows.Next() {
var info TrackCalendarInfo
if err := rows.Scan(&info.ID, &info.TrackID, &info.TrackName, &info.StartAt, &info.EndAt); err != nil {
return nil, fmt.Errorf("scan calendar track: %w", err)
}
out = append(out, info)
}
return out, nil
}
+310
View File
@@ -0,0 +1,310 @@
package races
import (
"context"
"os"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestLeaderboard(t *testing.T) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
t.Skip("DATABASE_URL is not set, skipping database integration tests")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("failed to connect to test database: %v", err)
}
defer pool.Close()
store := NewPgStore(pool)
// Clean up any left-overs with our test prefix
cleanup := func() {
_, _ = pool.Exec(ctx, "DELETE FROM race_drivers WHERE race_id LIKE 'test-race-%'")
_, _ = pool.Exec(ctx, "DELETE FROM races WHERE id LIKE 'test-race-%'")
_, _ = pool.Exec(ctx, "DELETE FROM drivers WHERE id LIKE 'test-driver-%'")
_, _ = pool.Exec(ctx, "DELETE FROM clans WHERE id LIKE 'test-clan-%'")
}
cleanup()
defer cleanup()
// 1. Create a test clan and test drivers
nowMs := time.Now().UnixMilli()
_, err = pool.Exec(ctx, `
INSERT INTO clans (id, tag, name, avatar_url, created_ms, updated_ms)
VALUES ('test-clan-1', 'TST', 'Test Clan', 'http://avatar.clan', $1, $1)`, nowMs)
if err != nil {
t.Fatalf("failed to insert test clan: %v", err)
}
driversData := []struct {
id string
nick string
name string
}{
{"test-driver-1", "DRV", "Driver One"},
{"test-driver-2", "TWO", "Driver Two"},
{"test-driver-3", "THR", "Driver Three"},
{"test-driver-4", "FOR", "Driver Four"},
}
for _, d := range driversData {
_, err = pool.Exec(ctx, `
INSERT INTO drivers (id, nickname, name, avatar_url, clan_id, created_ms, updated_ms)
VALUES ($1, $2, $3, 'http://avatar', 'test-clan-1', $4, $4)`,
d.id, d.nick, d.name, nowMs)
if err != nil {
t.Fatalf("failed to insert test driver %s: %v", d.id, err)
}
}
// 2. Create test races on different tracks for current year
currentYear := time.Now().Year()
testYearMs := time.Date(currentYear, time.June, 1, 12, 0, 0, 0, time.UTC).UnixMilli()
racesData := []struct {
id string
trackID string
}{
{"test-race-1", "test-track-A"},
{"test-race-2", "test-track-A"},
{"test-race-3", "test-track-B"},
}
for _, r := range racesData {
_, err = pool.Exec(ctx, `
INSERT INTO races (id, name, track_id, max_cars, laps, time_limit_s, status, created_ms, started_ms, finished_ms, updated_ms)
VALUES ($1, 'Test Race', $2, 4, 10, 60, 'finished', $3, $3, $3, $3)`,
r.id, r.trackID, testYearMs)
if err != nil {
t.Fatalf("failed to insert test race %s: %v", r.id, err)
}
}
// 3. Insert race drivers results
// Race 1 (Track A):
// - test-driver-1: best_lap 100 (1st fastest -> 25 points)
// - test-driver-2: best_lap 400 (currently 3rd fastest -> 25 points)
// - test-driver-3: best_lap 300 (currently 2nd fastest -> 25 points)
// - test-driver-4: best_lap 0 (DNF, completed 0 laps -> 1 point)
resultsRace1 := []struct {
driverID string
bestLapMs int64
pos any
}{
{"test-driver-1", 100, 1},
{"test-driver-2", 400, 2},
{"test-driver-3", 300, 3},
{"test-driver-4", 0, nil},
}
for _, res := range resultsRace1 {
_, err = pool.Exec(ctx, `
INSERT INTO race_drivers (race_id, driver_id, slot, joined_ms, total_time_ms, best_lap_ms, position)
VALUES ('test-race-1', $1, 0, $2, 1000, $3, $4)`,
res.driverID, testYearMs, res.bestLapMs, res.pos)
if err != nil {
t.Fatalf("failed to insert race driver result: %v", err)
}
}
// Race 2 (Track A - testing best results):
// - test-driver-2: best_lap 150 (2nd fastest -> 25 points, overrides 400ms lap time)
// This makes the times:
// - test-driver-1: 100ms (rank 1 -> 25 points)
// - test-driver-2: 150ms (rank 2 -> 25 points)
// - test-driver-3: 300ms (rank 3 -> 25 points)
// - test-driver-4: NULL (rank 4 -> 1 point)
// Exactly 3 drivers have 25 points.
_, err = pool.Exec(ctx, `
INSERT INTO race_drivers (race_id, driver_id, slot, joined_ms, total_time_ms, best_lap_ms, position)
VALUES ('test-race-2', 'test-driver-2', 0, $1, 1000, 150, 1)`, testYearMs)
if err != nil {
t.Fatalf("failed to insert race 2 result: %v", err)
}
// Race 3 (Track B):
// - test-driver-1: best_lap 500 (rank 2 on Track B -> 25 points)
// - test-driver-3: best_lap 200 (rank 1 on Track B -> 25 points)
resultsRace3 := []struct {
driverID string
bestLapMs int64
pos any
}{
{"test-driver-1", 500, 2},
{"test-driver-3", 200, 1},
}
for _, res := range resultsRace3 {
_, err = pool.Exec(ctx, `
INSERT INTO race_drivers (race_id, driver_id, slot, joined_ms, total_time_ms, best_lap_ms, position)
VALUES ('test-race-3', $1, 0, $2, 1000, $3, $4)`,
res.driverID, testYearMs, res.bestLapMs, res.pos)
if err != nil {
t.Fatalf("failed to insert race 3 driver result: %v", err)
}
}
// Test Track A Leaderboard
t.Run("Track A Leaderboard", func(t *testing.T) {
entries, total, err := store.GetTrackLeaderboard(ctx, "test-track-A", currentYear, 10, 0)
if err != nil {
t.Fatalf("failed to get track leaderboard: %v", err)
}
if total != 4 {
t.Errorf("expected total 4, got %d", total)
}
if len(entries) != 4 {
t.Fatalf("expected 4 entries, got %d", len(entries))
}
// Rank 1: test-driver-1 (25 pts, best_time 100ms)
// Rank 2: test-driver-2 (25 pts, best_time 150ms)
// Rank 3: test-driver-3 (25 pts, best_time 300ms)
// Rank 4: test-driver-4 (1 pt, best_time NULL)
drv1 := entries[0]
if drv1.DriverID != "test-driver-1" || drv1.Points != 25 || *drv1.BestTimeMs != 100 || drv1.Rank != 1 {
t.Errorf("invalid rank 1: %+v", drv1)
}
drv2 := entries[1]
if drv2.DriverID != "test-driver-2" || drv2.Points != 25 || *drv2.BestTimeMs != 150 || drv2.Rank != 2 {
t.Errorf("invalid rank 2: %+v", drv2)
}
// Check entries by looping to find specific drivers
for _, e := range entries {
if e.DriverID == "test-driver-3" {
if e.Points != 25 || *e.BestTimeMs != 300 || e.Rank != 3 {
t.Errorf("invalid test-driver-3: %+v", e)
}
}
if e.DriverID == "test-driver-4" {
if e.Points != 1 || e.BestTimeMs != nil || e.Rank != 4 {
t.Errorf("invalid test-driver-4: %+v", e)
}
}
}
})
// Test Track A Driver Rank
t.Run("Track A Driver Rank", func(t *testing.T) {
entry, err := store.GetTrackLeaderboardDriver(ctx, "test-track-A", currentYear, "test-driver-2")
if err != nil {
t.Fatalf("failed to get driver rank: %v", err)
}
if entry == nil || entry.Rank != 2 || entry.Points != 25 || *entry.BestTimeMs != 150 {
t.Errorf("expected rank 2, points 25, best_time 150, got %+v", entry)
}
})
// Test Overall Leaderboard
t.Run("Overall Leaderboard", func(t *testing.T) {
// Overall points calculation:
// - test-driver-1: Track A (25) + Track B (25) = 50 points
// - test-driver-3: Track A (25) + Track B (25) = 50 points
// - test-driver-2: Track A (25) + Track B (0) = 25 points
// - test-driver-4: Track A (1) + Track B (0) = 1 point
entries, _, err := store.GetOverallLeaderboard(ctx, currentYear, 200, 0)
if err != nil {
t.Fatalf("failed to get overall leaderboard: %v", err)
}
// Filter to only our test drivers
var testEntries []LeaderboardEntry
for _, entry := range entries {
if len(entry.DriverID) >= 11 && entry.DriverID[:12] == "test-driver-" {
testEntries = append(testEntries, entry)
}
}
if len(testEntries) != 4 {
t.Fatalf("expected 4 test entries, got %d", len(testEntries))
}
// test-driver-1 and test-driver-3 should both have 50 points
if testEntries[0].Points != 50 || testEntries[1].Points != 50 {
t.Errorf("expected top 2 test drivers to have 50 points, got: %+v and %+v", testEntries[0], testEntries[1])
}
if testEntries[2].DriverID != "test-driver-2" || testEntries[2].Points != 25 {
t.Errorf("invalid overall rank 3: %+v", testEntries[2])
}
if testEntries[3].DriverID != "test-driver-4" || testEntries[3].Points != 1 {
t.Errorf("invalid overall rank 4: %+v", testEntries[3])
}
})
// Test Overall Driver Rank
t.Run("Overall Driver Rank", func(t *testing.T) {
entry, err := store.GetOverallLeaderboardDriver(ctx, currentYear, "test-driver-3")
if err != nil {
t.Fatalf("failed to get overall driver rank: %v", err)
}
if entry == nil || entry.Points != 50 || entry.Rank <= 0 {
t.Errorf("expected rank > 0, points 50, got %+v", entry)
}
})
// Test Fallback Profile
t.Run("Fallback Profile", func(t *testing.T) {
entry, err := store.GetDriverLeaderboardProfile(ctx, "test-driver-1")
if err != nil {
t.Fatalf("failed to get profile: %v", err)
}
if entry == nil || entry.Name != "Driver One" || entry.Nickname != "DRV" || entry.ClanTag != "TST" {
t.Errorf("expected Driver One, DRV, TST, got %+v", entry)
}
})
// Test Calendar Leaderboard
t.Run("Calendar Leaderboard", func(t *testing.T) {
_, _ = pool.Exec(ctx, "DELETE FROM track_calendar")
now := time.Now()
_, err = pool.Exec(ctx, `
INSERT INTO track_calendar (track_id, start_at, end_at) VALUES
('monaco', $1, $2),
('barcelona-catalunya', $3, $4),
('austria-redbull-ring', $5, $6)
`,
now.Add(-10*time.Hour), now.Add(-5*time.Hour),
now.Add(-2*time.Hour), now.Add(2*time.Hour),
now.Add(5*time.Hour), now.Add(10*time.Hour),
)
if err != nil {
t.Fatalf("failed to insert track calendar: %v", err)
}
defer func() {
_, _ = pool.Exec(ctx, "DELETE FROM track_calendar")
}()
svc := NewService(store, nil)
svc.now = func() time.Time { return now }
res, err := svc.GetCalendarLeaderboard(ctx, currentYear, "test-driver-1")
if err != nil {
t.Fatalf("failed to get calendar leaderboard: %v", err)
}
if len(res) != 3 {
t.Fatalf("expected 3 calendar entries, got %d", len(res))
}
// 1. Austria (planned)
if res[0].TrackID != "austria-redbull-ring" || res[0].Status != "planned" {
t.Errorf("expected austria-redbull-ring (planned), got %s (%s)", res[0].TrackID, res[0].Status)
}
// 2. Barcelona (active)
if res[1].TrackID != "barcelona-catalunya" || res[1].Status != "active" {
t.Errorf("expected barcelona-catalunya (active), got %s (%s)", res[1].TrackID, res[1].Status)
}
// 3. Monaco (finished)
if res[2].TrackID != "monaco" || res[2].Status != "finished" {
t.Errorf("expected monaco (finished), got %s (%s)", res[2].TrackID, res[2].Status)
}
})
}
+102 -29
View File
@@ -29,20 +29,35 @@ var DefaultNow = time.Now
// run time. If no tracks exist, the seeder falls back to ["default"]. // run time. If no tracks exist, the seeder falls back to ["default"].
var fallbackTracks = []string{"default"} var fallbackTracks = []string{"default"}
// Default driver nicknames used when the drivers table is empty. Three
// uppercase letters each (per the drivers table constraint).
var defaultDriverSeeds = []struct { var defaultDriverSeeds = []struct {
id string
nick string nick string
name string name string
avatarURL string
clanID string
createdMs int64
updatedMs int64
}{ }{
{"ACE", "Alice"}, {"driver-seed-001", "BOR", "Gabriel Bortoleto", "https://minio.is1di.ru/x0gp/avatar/gabriel-bortoleto-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584003},
{"BOB", "Bob"}, {"driver-seed-002", "HUL", "Nico Hulkenberg", "https://minio.is1di.ru/x0gp/avatar/nico-hulkenberg-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584007},
{"CAR", "Carol"}, {"driver-seed-003", "ALB", "Alex Albon", "https://minio.is1di.ru/x0gp/avatar/alex-albon-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584008},
{"DAV", "Dave"}, {"driver-seed-004", "SAI", "Carlos Sainz", "https://minio.is1di.ru/x0gp/avatar/carlos-sainz-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584011},
{"EVE", "Eve"}, {"driver-seed-005", "BEA", "Oliver Bearman", "https://minio.is1di.ru/x0gp/avatar/oliver-bearman-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584012},
{"FRA", "Frank"}, {"driver-seed-006", "OCO", "Esteban Ocon", "https://minio.is1di.ru/x0gp/avatar/esteban-ocon-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584014},
{"GRA", "Grace"}, {"driver-seed-007", "LIN", "Arvid Lindblad", "https://minio.is1di.ru/x0gp/avatar/arvid-lindblad-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584018},
{"HEI", "Heidi"}, {"driver-seed-008", "LAW", "Liam Lawson", "https://minio.is1di.ru/x0gp/avatar/liam-lawson-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584018},
{"driver-seed-009", "COL", "Franco Colapinto", "https://minio.is1di.ru/x0gp/avatar/franco-colapinto-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584020},
{"driver-seed-010", "GAS", "Pierre Gasly", "https://minio.is1di.ru/x0gp/avatar/pierre-gasly-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584023},
{"driver-seed-011", "STR", "Lance Stroll", "https://minio.is1di.ru/x0gp/avatar/lance-stroll-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584023},
{"driver-seed-012", "ALO", "Fernando Alonso", "https://minio.is1di.ru/x0gp/avatar/fernando-alonso-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584026},
{"driver-seed-013", "ANT", "Kimi Antonelli", "https://minio.is1di.ru/x0gp/avatar/kimi-antonelli-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584028},
{"driver-seed-014", "RUS", "George Russel", "https://minio.is1di.ru/x0gp/avatar/george-russell-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584028},
{"driver-seed-015", "HAD", "Isack Hadjar", "https://minio.is1di.ru/x0gp/avatar/isack-hadjar-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584031},
{"driver-seed-016", "VER", "Max Verstappen", "https://minio.is1di.ru/x0gp/avatar/max-verstappen-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584033},
{"driver-seed-017", "HAM", "Lewis Hamilton", "https://minio.is1di.ru/x0gp/avatar/lewis-hamilton-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584034},
{"driver-seed-018", "NOR", "Lando Norris", "https://minio.is1di.ru/x0gp/avatar/lando-norris-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584036},
{"driver-seed-019", "LEC", "Charles Leclerc", "https://minio.is1di.ru/x0gp/avatar/charles-leclerc-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584038},
{"driver-seed-020", "PIA", "Oscar Piastri", "https://minio.is1di.ru/x0gp/avatar/oscar-piastri-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584039},
} }
// Default clans. // Default clans.
@@ -226,10 +241,54 @@ func (r *Runner) Run(ctx context.Context, opt Options) (Summary, error) {
s.PlansInserted++ s.PlansInserted++
} }
if err := r.seedTrackCalendar(ctx); err != nil {
return s, fmt.Errorf("seed track calendar: %w", err)
}
s.Duration = time.Since(start) s.Duration = time.Since(start)
return s, nil return s, nil
} }
func (r *Runner) seedTrackCalendar(ctx context.Context) error {
now := r.now
if len(r.trackIDs) == 0 {
return nil
}
tracksMap := make(map[string]bool)
for _, id := range r.trackIDs {
tracksMap[id] = true
}
schedule := []struct {
trackID string
startAt time.Time
endAt time.Time
}{
{"monaco", now.Add(-14 * 24 * time.Hour), now.Add(-7 * 24 * time.Hour)},
{"barcelona-catalunya", now.Add(-2 * 24 * time.Hour), now.Add(12 * 24 * time.Hour)},
{"austria-redbull-ring", now.Add(14 * 24 * time.Hour), now.Add(28 * 24 * time.Hour)},
{"great-britain-silverstone", now.Add(28 * 24 * time.Hour), now.Add(42 * 24 * time.Hour)},
{"belgium-spa", now.Add(42 * 24 * time.Hour), now.Add(56 * 24 * time.Hour)},
}
for _, s := range schedule {
if !tracksMap[s.trackID] {
continue
}
_, err := r.pg.Exec(ctx, `
INSERT INTO track_calendar (track_id, start_at, end_at)
VALUES ($1, $2, $3)
`, s.trackID, s.startAt, s.endAt)
if err != nil {
return fmt.Errorf("insert calendar entry: %w", err)
}
}
return nil
}
type planShape struct { type planShape struct {
intervalS int intervalS int
count int count int
@@ -241,18 +300,31 @@ type planShape struct {
// cascade), plus the plan and queue tables. Drivers and clans are // cascade), plus the plan and queue tables. Drivers and clans are
// kept (they're independent of races). // kept (they're independent of races).
func (r *Runner) resetAll(ctx context.Context) error { func (r *Runner) resetAll(ctx context.Context) error {
if _, err := r.pg.Exec(ctx, if _, err := r.pg.Exec(ctx, `DELETE FROM race_queue`); err != nil {
`DELETE FROM races WHERE status IN ('finished','cancelled')`); err != nil {
return err return err
} }
if _, err := r.pg.Exec(ctx, if _, err := r.pg.Exec(ctx, `DELETE FROM races`); err != nil {
`TRUNCATE TABLE race_plans, race_queue`); err != nil { return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM race_plans`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM track_calendar`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM drivers`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM clans`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `ALTER SEQUENCE IF EXISTS track_calendar_id_seq RESTART WITH 1`); err != nil {
return err return err
} }
for _, m := range r.lb.ListRaces() { for _, m := range r.lb.ListRaces() {
_ = r.lb.DeleteRace(m.ID) _ = r.lb.DeleteRace(m.ID)
} }
if _, err := r.pg.Exec(ctx, `TRUNCATE TABLE lobby_drivers`); err != nil { if _, err := r.pg.Exec(ctx, `DELETE FROM lobby_drivers`); err != nil {
return err return err
} }
return nil return nil
@@ -286,21 +358,15 @@ func (r *Runner) seedDriversAndClans(ctx context.Context) error {
return fmt.Errorf("count drivers: %w", err) return fmt.Errorf("count drivers: %w", err)
} }
if drvCount == 0 { if drvCount == 0 {
now := time.Now().UnixMilli() for _, ds := range defaultDriverSeeds {
// Round-robin: each driver assigned to one of the seeded clans.
clanIDs := make([]string, 0, len(defaultClanSeeds))
for i := range defaultClanSeeds {
clanIDs = append(clanIDs, fmt.Sprintf("clan-seed-%03d", i+1))
}
for i, ds := range defaultDriverSeeds {
d := drivers.Driver{ d := drivers.Driver{
ID: fmt.Sprintf("driver-seed-%03d", i+1), ID: ds.id,
Nickname: ds.nick, Nickname: ds.nick,
Name: ds.name, Name: ds.name,
AvatarURL: fmt.Sprintf("https://cdn.example.com/drivers/%s.png", ds.nick), AvatarURL: ds.avatarURL,
ClanID: clanIDs[i%len(clanIDs)], ClanID: ds.clanID,
CreatedMs: now, CreatedMs: ds.createdMs,
UpdatedMs: now, UpdatedMs: ds.updatedMs,
} }
if err := r.driversPg.Create(ctx, d); err != nil { if err := r.driversPg.Create(ctx, d); err != nil {
return fmt.Errorf("insert driver %s: %w", ds.nick, err) return fmt.Errorf("insert driver %s: %w", ds.nick, err)
@@ -481,8 +547,15 @@ func (r *Runner) createLive(status lobby.RaceStatus, idx int) (lobby.RaceMeta, e
continue continue
} }
r.lb.SetDriverProfile(d.ID, d.Nickname, "", d.ClanID, d.ClanTag) r.lb.SetDriverProfile(d.ID, d.Nickname, "", d.ClanID, d.ClanTag)
devID := i cars := []string{
_ = r.lb.SelectDevice(d.ID, &devID) "f1-2024-redbull-rb20",
"f1-2024-ferrari-sf24",
"f1-2024-mclaren-mcl38",
"f1-2024-mercedes-w15",
"f1-2024-astonmartin-amr24",
}
carID := cars[i%len(cars)]
_ = r.lb.SelectCar(d.ID, &carID)
if err := r.lb.AddDriverToRace(d.ID, meta.ID); err != nil { if err := r.lb.AddDriverToRace(d.ID, meta.ID); err != nil {
// ignore: race may be at capacity // ignore: race may be at capacity
} }
+129 -2
View File
@@ -62,8 +62,8 @@ func (s *Service) RestoreFromDB(ctx context.Context) (int, int, error) {
// AddDriver+SetDriverProfile and accept a no-op mirror. // AddDriver+SetDriverProfile and accept a no-op mirror.
_, _ = s.lobby.AddDriver(d.ID, d.Name, "") _, _ = s.lobby.AddDriver(d.ID, d.Name, "")
s.lobby.SetDriverProfile(d.ID, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag) s.lobby.SetDriverProfile(d.ID, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag)
if d.DeviceID != nil { if d.CarID != nil {
_ = s.lobby.SelectDevice(d.ID, d.DeviceID) _ = s.lobby.SelectCar(d.ID, d.CarID)
} }
} }
for _, r := range races { for _, r := range races {
@@ -685,6 +685,133 @@ func (s *Service) DeletePlan(ctx context.Context, id string) error {
return s.pg.DeletePlan(ctx, id) return s.pg.DeletePlan(ctx, id)
} }
// GetLeaderboard retrieves the leaderboard page and current driver position.
func (s *Service) GetLeaderboard(ctx context.Context, trackID string, year int, limit, offset int, currentDriverID string) (*LeaderboardResult, error) {
if year == 0 {
year = s.now().Year()
}
var items []LeaderboardEntry
var total int
var err error
if trackID != "" {
items, total, err = s.pg.GetTrackLeaderboard(ctx, trackID, year, limit, offset)
} else {
items, total, err = s.pg.GetOverallLeaderboard(ctx, year, limit, offset)
}
if err != nil {
return nil, err
}
var currentDriver *LeaderboardEntry
if currentDriverID != "" {
if trackID != "" {
currentDriver, err = s.pg.GetTrackLeaderboardDriver(ctx, trackID, year, currentDriverID)
} else {
currentDriver, err = s.pg.GetOverallLeaderboardDriver(ctx, year, currentDriverID)
}
if err != nil {
return nil, err
}
if currentDriver == nil {
// fallback: get profile from drivers table
currentDriver, err = s.pg.GetDriverLeaderboardProfile(ctx, currentDriverID)
if err != nil {
return nil, err
}
// if driver exists but has no points, set points/rank to 0
if currentDriver != nil {
currentDriver.Points = 0
currentDriver.Rank = 0
}
}
}
if items == nil {
items = []LeaderboardEntry{}
}
return &LeaderboardResult{
Items: items,
Total: total,
CurrentDriver: currentDriver,
}, nil
}
// GetCalendarLeaderboard returns the leaderboard details of all scheduled tracks.
func (s *Service) GetCalendarLeaderboard(ctx context.Context, year int, currentDriverID string) ([]TrackCalendarLeaderboard, error) {
if year == 0 {
year = s.now().Year()
}
calendarInfo, err := s.pg.GetCalendarLeaderboardTracks(ctx)
if err != nil {
return nil, err
}
now := s.now()
var out []TrackCalendarLeaderboard
// Cache driver profile fallback if we need it
var fallbackProfile *LeaderboardEntry
if currentDriverID != "" {
fallbackProfile, err = s.pg.GetDriverLeaderboardProfile(ctx, currentDriverID)
if err != nil {
return nil, err
}
if fallbackProfile != nil {
fallbackProfile.Points = 0
fallbackProfile.Rank = 0
}
}
for _, cal := range calendarInfo {
status := "planned"
if now.After(cal.EndAt) {
status = "finished"
} else if (now.Equal(cal.StartAt) || now.After(cal.StartAt)) && now.Before(cal.EndAt) {
status = "active"
}
// Get top 3
podium, _, err := s.pg.GetTrackLeaderboard(ctx, cal.TrackID, year, 3, 0)
if err != nil {
return nil, err
}
if podium == nil {
podium = []LeaderboardEntry{}
}
// Get current driver
var curDriver *LeaderboardEntry
if currentDriverID != "" {
curDriver, err = s.pg.GetTrackLeaderboardDriver(ctx, cal.TrackID, year, currentDriverID)
if err != nil {
return nil, err
}
if curDriver == nil && fallbackProfile != nil {
// Clone the fallback profile so we don't share pointer/modify original
profileCopy := *fallbackProfile
curDriver = &profileCopy
}
}
out = append(out, TrackCalendarLeaderboard{
TrackID: cal.TrackID,
TrackName: cal.TrackName,
StartAt: cal.StartAt,
EndAt: cal.EndAt,
Status: status,
Podium: podium,
CurrentDriver: curDriver,
})
}
return out, nil
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Scheduler // Scheduler
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+7 -7
View File
@@ -112,8 +112,8 @@ func NewPgStore(pool *pgxpool.Pool) *PgStore {
} }
// Exec exposes a raw Exec for the seeder / maintenance scripts. // Exec exposes a raw Exec for the seeder / maintenance scripts.
func (s *PgStore) Exec(ctx context.Context, sql string) (int64, error) { func (s *PgStore) Exec(ctx context.Context, sql string, args ...any) (int64, error) {
tag, err := s.pool.Exec(ctx, sql) tag, err := s.pool.Exec(ctx, sql, args...)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -391,7 +391,7 @@ func (s *PgStore) UpsertLobbyDriver(ctx context.Context, d lobby.DriverMeta) err
_, err := s.pool.Exec(ctx, ` _, err := s.pool.Exec(ctx, `
INSERT INTO lobby_drivers INSERT INTO lobby_drivers
(driver_id, name, nickname, avatar_url, clan_id, clan_tag, (driver_id, name, nickname, avatar_url, clan_id, clan_tag,
status, current_race_id, connected_ms, last_seen_ms, device_id) status, current_race_id, connected_ms, last_seen_ms, car_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (driver_id) DO UPDATE SET ON CONFLICT (driver_id) DO UPDATE SET
name = EXCLUDED.name, name = EXCLUDED.name,
@@ -402,9 +402,9 @@ func (s *PgStore) UpsertLobbyDriver(ctx context.Context, d lobby.DriverMeta) err
status = EXCLUDED.status, status = EXCLUDED.status,
current_race_id = EXCLUDED.current_race_id, current_race_id = EXCLUDED.current_race_id,
last_seen_ms = EXCLUDED.last_seen_ms, last_seen_ms = EXCLUDED.last_seen_ms,
device_id = EXCLUDED.device_id`, car_id = EXCLUDED.car_id`,
d.ID, d.Name, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag, d.ID, d.Name, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag,
string(d.Status), d.RaceID, d.ConnectedMs, d.LastSeenMs, d.DeviceID) string(d.Status), d.RaceID, d.ConnectedMs, d.LastSeenMs, d.CarID)
if err != nil { if err != nil {
return fmt.Errorf("upsert lobby driver %s: %w", d.ID, err) return fmt.Errorf("upsert lobby driver %s: %w", d.ID, err)
} }
@@ -421,7 +421,7 @@ func (s *PgStore) DeleteLobbyDriver(ctx context.Context, driverID string) error
func (s *PgStore) ListLobbyDrivers(ctx context.Context) ([]lobby.DriverMeta, error) { func (s *PgStore) ListLobbyDrivers(ctx context.Context) ([]lobby.DriverMeta, error) {
rows, err := s.pool.Query(ctx, ` rows, err := s.pool.Query(ctx, `
SELECT driver_id, name, nickname, avatar_url, clan_id, clan_tag, SELECT driver_id, name, nickname, avatar_url, clan_id, clan_tag,
status, current_race_id, connected_ms, last_seen_ms, device_id status, current_race_id, connected_ms, last_seen_ms, car_id
FROM lobby_drivers ORDER BY last_seen_ms DESC`) FROM lobby_drivers ORDER BY last_seen_ms DESC`)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -432,7 +432,7 @@ func (s *PgStore) ListLobbyDrivers(ctx context.Context) ([]lobby.DriverMeta, err
var d lobby.DriverMeta var d lobby.DriverMeta
var status string var status string
if err := rows.Scan(&d.ID, &d.Name, &d.Nickname, &d.AvatarURL, &d.ClanID, &d.ClanTag, if err := rows.Scan(&d.ID, &d.Name, &d.Nickname, &d.AvatarURL, &d.ClanID, &d.ClanTag,
&status, &d.RaceID, &d.ConnectedMs, &d.LastSeenMs, &d.DeviceID); err != nil { &status, &d.RaceID, &d.ConnectedMs, &d.LastSeenMs, &d.CarID); err != nil {
return nil, err return nil, err
} }
d.Status = lobby.DriverStatus(status) d.Status = lobby.DriverStatus(status)
@@ -8,81 +8,81 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27, VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27,
3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen', 3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen',
0.4, 1.4, 3.95, 2.5, 0.4, -2.5, 3.95, -1.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 0, 0.4, 2.2, 0, 3.2, 0) VALUES ('monaco', 0, 0.4, -2.2, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 1, 1.1, 2.2, 0, 3, 0) VALUES ('monaco', 1, 1.1, -2.2, -0, 3, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 2, 1.5, 2.05, -0.4, 2.4, 1.2) VALUES ('monaco', 2, 1.5, -2.05, 0.4, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 3, 1.7, 1.85, -0.9, 2.2, 1.8) VALUES ('monaco', 3, 1.7, -1.85, 0.9, 2.2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 4, 1.55, 1.55, -1.6, 2, 1.4) VALUES ('monaco', 4, 1.55, -1.55, 1.6, 2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 5, 1.25, 1.4, -2.4, 2.1, 1.2) VALUES ('monaco', 5, 1.25, -1.4, 2.4, 2.1, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 6, 0.95, 1.55, 3.141592653589793, 1.9, 1.6) VALUES ('monaco', 6, 0.95, -1.55, -3.141592653589793, 1.9, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 7, 0.75, 1.85, 2, 1.8, 2.2) VALUES ('monaco', 7, 0.75, -1.85, -2, 1.8, 2.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 8, 0.95, 2.15, 2.6, 1.8, 2.6) VALUES ('monaco', 8, 0.95, -2.15, -2.6, 1.8, 2.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 9, 1.2, 2.4, -2.8, 2.1, 1.4) VALUES ('monaco', 9, 1.2, -2.4, 2.8, 2.1, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 10, 1.6, 2.5, -3, 2.4, 1) VALUES ('monaco', 10, 1.6, -2.5, 3, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 11, 2.6, 2.5, 0, 3.2, 0) VALUES ('monaco', 11, 2.6, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 12, 3.4, 2.5, 0, 3.2, 0) VALUES ('monaco', 12, 3.4, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 13, 3.6, 2.25, -0.5, 2.6, 1.5) VALUES ('monaco', 13, 3.6, -2.25, 0.5, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 14, 3.55, 1.95, 0.4, 2.6, 1.5) VALUES ('monaco', 14, 3.55, -1.95, -0.4, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 15, 3.3, 1.75, 1, 2.3, 1.4) VALUES ('monaco', 15, 3.3, -1.75, -1, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 16, 2.95, 1.55, 1.7, 2.1, 1.8) VALUES ('monaco', 16, 2.95, -1.55, -1.7, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 17, 2.6, 1.65, 2.6, 2.1, 1.8) VALUES ('monaco', 17, 2.6, -1.65, -2.6, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 18, 2.4, 1.9, -2.7, 2.3, 1.4) VALUES ('monaco', 18, 2.4, -1.9, 2.7, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 19, 2.4, 2.2, -3, 2.2, 1.6) VALUES ('monaco', 19, 2.4, -2.2, 3, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 20, 2.6, 2.4, 2.8, 2.5, 1) VALUES ('monaco', 20, 2.6, -2.4, -2.8, 2.5, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 21, 3.2, 2.4, 0, 3.2, 0) VALUES ('monaco', 21, 3.2, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 22, 3.7, 2.4, 0, 3.2, 0) VALUES ('monaco', 22, 3.7, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 23, 3.95, 2.3, -0.3, 2.8, 0.8) VALUES ('monaco', 23, 3.95, -2.3, 0.3, 2.8, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1')
@@ -102,93 +102,93 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27, VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27,
3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen', 3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen',
0.3, 0.4, 4.05, 2.7, 0.3, -2.7, 4.05, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 0, 0.3, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 0, 0.3, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 1, 1.4, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 1, 1.4, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 2, 1.7, 0.65, 1.5707963267948966, 2.2, 1.6) VALUES ('barcelona-catalunya', 2, 1.7, -0.65, -1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 3, 1.5, 0.95, 3.141592653589793, 2, 2) VALUES ('barcelona-catalunya', 3, 1.5, -0.95, -3.141592653589793, 2, 2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 4, 1.2, 1.1, 3.4415926535897934, 2.6, 1) VALUES ('barcelona-catalunya', 4, 1.2, -1.1, -3.4415926535897934, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 5, 0.95, 1, 4.0415926535897935, 2.6, 1) VALUES ('barcelona-catalunya', 5, 0.95, -1, -4.0415926535897935, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 6, 0.8, 1.2, -1.5707963267948966, 2.4, 1.2) VALUES ('barcelona-catalunya', 6, 0.8, -1.2, 1.5707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 7, 1.05, 1.5, 0, 3.2, 0.7) VALUES ('barcelona-catalunya', 7, 1.05, -1.5, -0, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 8, 1.3, 1.65, 1.5707963267948966, 2.4, 1) VALUES ('barcelona-catalunya', 8, 1.3, -1.65, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 9, 1.15, 1.85, 3.141592653589793, 2.4, 1) VALUES ('barcelona-catalunya', 9, 1.15, -1.85, -3.141592653589793, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 10, 1.4, 2, 2.641592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 10, 1.4, -2, -2.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 11, 1.7, 2, 2.141592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 11, 1.7, -2, -2.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 12, 1.95, 2.2, 1.5707963267948966, 2.8, 0.5) VALUES ('barcelona-catalunya', 12, 1.95, -2.2, -1.5707963267948966, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 13, 1.65, 2.5, 0.3, 2.8, 0.5) VALUES ('barcelona-catalunya', 13, 1.65, -2.5, -0.3, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 14, 2.2, 2.7, 0.7, 3.2, 0.5) VALUES ('barcelona-catalunya', 14, 2.2, -2.7, -0.7, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 15, 2.8, 2.7, 1.1, 3.4, 0.5) VALUES ('barcelona-catalunya', 15, 2.8, -2.7, -1.1, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 16, 3.1, 2.5, 1.9707963267948967, 2.2, 1.4) VALUES ('barcelona-catalunya', 16, 3.1, -2.5, -1.9707963267948967, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 17, 2.95, 2.2, 3.541592653589793, 2.2, 1.4) VALUES ('barcelona-catalunya', 17, 2.95, -2.2, -3.541592653589793, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 18, 3.3, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 18, 3.3, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 19, 4, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 19, 4, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 20, 4.05, 1.6, -1.5707963267948966, 2.4, 1.4) VALUES ('barcelona-catalunya', 20, 4.05, -1.6, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 21, 3.8, 1.4, 3.141592653589793, 2.4, 1.4) VALUES ('barcelona-catalunya', 21, 3.8, -1.4, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 22, 3.5, 1.5, 3.9269908169872414, 2.6, 1) VALUES ('barcelona-catalunya', 22, 3.5, -1.5, -3.9269908169872414, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 23, 3.1, 1.25, -1.9707963267948967, 3, 0.7) VALUES ('barcelona-catalunya', 23, 3.1, -1.25, 1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 24, 2.7, 1, -2.4707963267948965, 3, 0.7) VALUES ('barcelona-catalunya', 24, 2.7, -1, 2.4707963267948965, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 25, 2.3, 0.7, 3.541592653589793, 3.4, 0.5) VALUES ('barcelona-catalunya', 25, 2.3, -0.7, -3.541592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 26, 1.6, 0.5, 3.3415926535897933, 3.6, 0.4) VALUES ('barcelona-catalunya', 26, 1.6, -0.5, -3.3415926535897933, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 27, 0.9, 0.42, 0, 4, 0) VALUES ('barcelona-catalunya', 27, 0.9, -0.42, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1')
@@ -206,84 +206,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27, VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27,
3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen', 3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen',
0.3, 0.4, 3.85, 2.4, 0.3, -2.4, 3.85, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 0, 0.3, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 0, 0.3, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 1, 1.6, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 1, 1.6, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 2, 1.85, 0.65, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 2, 1.85, -0.65, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 3, 1.6, 0.95, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 3, 1.6, -0.95, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 4, 1.3, 1, 3.641592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 4, 1.3, -1, -3.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 5, 1.05, 1.2, -1.5707963267948966, 2.6, 1) VALUES ('austria-redbull-ring', 5, 1.05, -1.2, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 6, 1.4, 1.4, 0, 3.2, 0.5) VALUES ('austria-redbull-ring', 6, 1.4, -1.4, -0, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 7, 1.85, 1.4, 1.7707963267948965, 3.2, 0.5) VALUES ('austria-redbull-ring', 7, 1.85, -1.4, -1.7707963267948965, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 8, 2.1, 1.65, 3.141592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 8, 2.1, -1.65, -3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 9, 2, 1.85, -1.5707963267948966, 3, 0.7) VALUES ('austria-redbull-ring', 9, 2, -1.85, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 10, 2.25, 2.05, 0, 2, 1.8) VALUES ('austria-redbull-ring', 10, 2.25, -2.05, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 11, 2.4, 2, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 11, 2.4, -2, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 12, 2.4, 2.2, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 12, 2.4, -2.2, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 13, 2.2, 2.3, -1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 13, 2.2, -2.3, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 14, 2.55, 2.4, -0.2, 3.4, 0.4) VALUES ('austria-redbull-ring', 14, 2.55, -2.4, 0.2, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 15, 3, 2.4, 0, 4, 0) VALUES ('austria-redbull-ring', 15, 3, -2.4, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 16, 3.5, 2.4, 0, 4.5, 0) VALUES ('austria-redbull-ring', 16, 3.5, -2.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 17, 3.85, 2.15, 1.7707963267948965, 3.4, 0.6) VALUES ('austria-redbull-ring', 17, 3.85, -2.15, -1.7707963267948965, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 18, 3.6, 1.8, 3.141592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 18, 3.6, -1.8, -3.141592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 19, 3.25, 1.85, 3.741592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 19, 3.25, -1.85, -3.741592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 20, 3, 1.6, -1.9707963267948967, 3.2, 0.6) VALUES ('austria-redbull-ring', 20, 3, -1.6, 1.9707963267948967, 3.2, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 21, 2.6, 1.4, -3.141592653589793, 3.4, 0.5) VALUES ('austria-redbull-ring', 21, 2.6, -1.4, 3.141592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 22, 2, 1.2, 2.641592653589793, 3.6, 0.4) VALUES ('austria-redbull-ring', 22, 2, -1.2, -2.641592653589793, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 23, 1.4, 0.8, 2.9415926535897934, 3.8, 0.3) VALUES ('austria-redbull-ring', 23, 1.4, -0.8, -2.9415926535897934, 3.8, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 24, 0.8, 0.5, 0, 4.2, 0.2) VALUES ('austria-redbull-ring', 24, 0.8, -0.5, -0, 4.2, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1')
@@ -300,79 +300,100 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27, VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27,
3.5500000000000003, 1.2499999999999998, 0.18, 'carpet', 0, 'Hamilton', 3.5500000000000003, 2.4, 0.18, 'carpet', 0, 'Hamilton',
0.3, 1.8, 3.85, 3.05, 0.3, -2.65, 3.85, -0.25,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 0, 0.3, 2.8, 0, 5, 0) VALUES ('great-britain-silverstone', 0, 0.3, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 1, 2.4, 2.8, 0, 5, 0) VALUES ('great-britain-silverstone', 1, 2.4, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 2, 2.8, 2.65, -1.5707963267948966, 3, 0.8) VALUES ('great-britain-silverstone', 2, 2.8, -0.45, -1.5707963267948966, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 3, 2.6, 2.3, -3.141592653589793, 3.4, 0.6) VALUES ('great-britain-silverstone', 3, 2.6, -0.8, -3.141592653589793, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 4, 2.95, 2.05, -2.0707963267948966, 3.2, 0.7) VALUES ('great-britain-silverstone', 4, 2.95, -1.05, -2.0707963267948966, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 5, 3.3, 1.8, 0, 2.2, 1.6) VALUES ('great-britain-silverstone', 5, 3.3, -1.3, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 6, 3.6, 1.95, 1.5707963267948966, 2.2, 1.6) VALUES ('great-britain-silverstone', 6, 3.6, -1.15, 1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 7, 3.6, 2.3, 3.141592653589793, 3, 0.7) VALUES ('great-britain-silverstone', 7, 3.6, -0.8, 3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 8, 3.85, 2.55, 1.5707963267948966, 2.6, 1) VALUES ('great-britain-silverstone', 8, 3.85, -0.55, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 9, 3.55, 2.85, 0, 2.4, 1) VALUES ('great-britain-silverstone', 9, 3.55, -0.25, -0, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 10, 3.25, 2.85, 1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 10, 3.25, -0.25, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 11, 3.05, 2.55, -1.8707963267948966, 3.4, 0.5) VALUES ('great-britain-silverstone', 11, 3.05, -0.55, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 12, 2.85, 2.65, -3.141592653589793, 4, 0.3) VALUES ('great-britain-silverstone', 12, 2.85, -0.45, -3.141592653589793, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 13, 1.8, 2.8, 0, 4.6, 0) VALUES ('great-britain-silverstone', 13, 1.8, -0.3, -0, 4.6, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 14, 1.2, 2.95, 1.7707963267948965, 3.8, 0.4) VALUES ('great-britain-silverstone', 14, 1.2, -0.45, 1.7707963267948965, 3.8, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 15, 1, 2.65, 3.541592653589793, 3.6, 0.5) VALUES ('great-britain-silverstone', 15, 1, -0.85, 3.541592653589793, 3.6, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 16, 0.7, 2.4, -1.8707963267948966, 2.4, 1.2) VALUES ('great-britain-silverstone', 16, 0.7, -1.15, -1.8707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 17, 0.5, 2.6, 0, 2.6, 0.9) VALUES ('great-britain-silverstone', 17, 0.5, -0.9, -0, 2.6, 0.9)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 18, 0.7, 2.85, 1.5707963267948966, 3, 0.7) VALUES ('great-britain-silverstone', 18, 0.7, -0.65, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 19, 0.45, 3.05, 3.541592653589793, 2.6, 1) VALUES ('great-britain-silverstone', 19, 0.45, -0.45, 3.541592653589793, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 20, 0.7, 2.5, -1.1707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 20, 0.75, -1, -1.1707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 21, 0.5, 2.05, 3.541592653589793, 2.6, 0.8) VALUES ('great-britain-silverstone', 21, 0.5, -1.5, 3.541592653589793, 2.6, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 22, 0.3, 2.4, 1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 22, 0.3, -1.15, 1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 23, 0.7, -1.6, -0.7853981633974483, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 24, 2, -1.95, -1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 25, 3, -2.4, -3.3415926535897933, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 26, 2.4, -2.65, 1.8707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 27, 1.4, -2.6, 3.141592653589793, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 28, 0.5, -2.3, 2.8415926535897933, 4.8, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 29, 0.3, -1.7, 1.2707963267948965, 5, 0.1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1')
@@ -390,84 +411,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27, VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27,
3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen', 3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen',
0.4, 0.5, 4.25, 2.75, 0.4, -2.75, 4.25, -0.5,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 0, 0.4, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 0, 0.4, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 1, 1.6, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 1, 1.6, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 2, 1.95, 0.6, 0.3, 4.2, 0.5) VALUES ('belgium-spa', 2, 1.95, -0.6, -0.3, 4.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 3, 2.2, 0.95, 0.9, 4, 0.6) VALUES ('belgium-spa', 3, 2.2, -0.95, -0.9, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 4, 2.1, 1.3, 3.541592653589793, 4, 0.6) VALUES ('belgium-spa', 4, 2.1, -1.3, -3.541592653589793, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 5, 2.7, 1.5, 0, 5, 0) VALUES ('belgium-spa', 5, 2.7, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 6, 3.5, 1.5, 0, 5, 0) VALUES ('belgium-spa', 6, 3.5, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 7, 3.85, 1.3, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 7, 3.85, -1.3, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 8, 3.6, 1.1, 3.4415926535897934, 2.6, 1.2) VALUES ('belgium-spa', 8, 3.6, -1.1, -3.4415926535897934, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 9, 3.3, 1.25, 4.0415926535897935, 2.6, 1.2) VALUES ('belgium-spa', 9, 3.3, -1.25, -4.0415926535897935, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 10, 3.5, 1.55, 1.5707963267948966, 3.6, 0.4) VALUES ('belgium-spa', 10, 3.5, -1.55, -1.5707963267948966, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 11, 3.25, 1.85, 0, 2.2, 1.6) VALUES ('belgium-spa', 11, 3.25, -1.85, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 12, 3.05, 1.7, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 12, 3.05, -1.7, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 13, 3.25, 1.5, 0, 3, 0.7) VALUES ('belgium-spa', 13, 3.25, -1.5, -0, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 14, 3.7, 1.65, 1.8707963267948966, 3.4, 0.5) VALUES ('belgium-spa', 14, 3.7, -1.65, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 15, 4.1, 2, 2.1707963267948966, 4, 0.3) VALUES ('belgium-spa', 15, 4.1, -2, -2.1707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 16, 4.25, 2.4, 3.541592653589793, 4.6, 0.3) VALUES ('belgium-spa', 16, 4.25, -2.4, -3.541592653589793, 4.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 17, 4, 2.7, -1.8707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 17, 4, -2.7, 1.8707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 18, 3.7, 2.55, 3.141592653589793, 2.4, 1.4) VALUES ('belgium-spa', 18, 3.7, -2.55, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 19, 3.55, 2.75, 3.8415926535897933, 2.4, 1.4) VALUES ('belgium-spa', 19, 3.55, -2.75, -3.8415926535897933, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 20, 3.05, 2.7, -1.5707963267948966, 2, 1.8) VALUES ('belgium-spa', 20, 3.05, -2.7, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 21, 2.85, 2.45, 0, 2, 1.8) VALUES ('belgium-spa', 21, 2.85, -2.45, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 22, 2, 2.2, 2.8415926535897933, 3.6, 0.3) VALUES ('belgium-spa', 22, 2, -2.2, -2.8415926535897933, 3.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 23, 1.2, 1.6, 2.541592653589793, 4, 0.2) VALUES ('belgium-spa', 23, 1.2, -1.6, -2.541592653589793, 4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 24, 0.6, 1.05, 2.8415926535897933, 4.4, 0.2) VALUES ('belgium-spa', 24, 0.6, -1.05, -2.8415926535897933, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1')
@@ -14,81 +14,81 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27, VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27,
3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen', 3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen',
0.4, 1.4, 3.95, 2.5, 0.4, -2.5, 3.95, -1.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 0, 0.4, 2.2, 0, 3.2, 0) VALUES ('monaco', 0, 0.4, -2.2, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 1, 1.1, 2.2, 0, 3, 0) VALUES ('monaco', 1, 1.1, -2.2, -0, 3, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 2, 1.5, 2.05, -0.4, 2.4, 1.2) VALUES ('monaco', 2, 1.5, -2.05, 0.4, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 3, 1.7, 1.85, -0.9, 2.2, 1.8) VALUES ('monaco', 3, 1.7, -1.85, 0.9, 2.2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 4, 1.55, 1.55, -1.6, 2, 1.4) VALUES ('monaco', 4, 1.55, -1.55, 1.6, 2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 5, 1.25, 1.4, -2.4, 2.1, 1.2) VALUES ('monaco', 5, 1.25, -1.4, 2.4, 2.1, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 6, 0.95, 1.55, 3.141592653589793, 1.9, 1.6) VALUES ('monaco', 6, 0.95, -1.55, -3.141592653589793, 1.9, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 7, 0.75, 1.85, 2, 1.8, 2.2) VALUES ('monaco', 7, 0.75, -1.85, -2, 1.8, 2.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 8, 0.95, 2.15, 2.6, 1.8, 2.6) VALUES ('monaco', 8, 0.95, -2.15, -2.6, 1.8, 2.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 9, 1.2, 2.4, -2.8, 2.1, 1.4) VALUES ('monaco', 9, 1.2, -2.4, 2.8, 2.1, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 10, 1.6, 2.5, -3, 2.4, 1) VALUES ('monaco', 10, 1.6, -2.5, 3, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 11, 2.6, 2.5, 0, 3.2, 0) VALUES ('monaco', 11, 2.6, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 12, 3.4, 2.5, 0, 3.2, 0) VALUES ('monaco', 12, 3.4, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 13, 3.6, 2.25, -0.5, 2.6, 1.5) VALUES ('monaco', 13, 3.6, -2.25, 0.5, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 14, 3.55, 1.95, 0.4, 2.6, 1.5) VALUES ('monaco', 14, 3.55, -1.95, -0.4, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 15, 3.3, 1.75, 1, 2.3, 1.4) VALUES ('monaco', 15, 3.3, -1.75, -1, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 16, 2.95, 1.55, 1.7, 2.1, 1.8) VALUES ('monaco', 16, 2.95, -1.55, -1.7, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 17, 2.6, 1.65, 2.6, 2.1, 1.8) VALUES ('monaco', 17, 2.6, -1.65, -2.6, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 18, 2.4, 1.9, -2.7, 2.3, 1.4) VALUES ('monaco', 18, 2.4, -1.9, 2.7, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 19, 2.4, 2.2, -3, 2.2, 1.6) VALUES ('monaco', 19, 2.4, -2.2, 3, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 20, 2.6, 2.4, 2.8, 2.5, 1) VALUES ('monaco', 20, 2.6, -2.4, -2.8, 2.5, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 21, 3.2, 2.4, 0, 3.2, 0) VALUES ('monaco', 21, 3.2, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 22, 3.7, 2.4, 0, 3.2, 0) VALUES ('monaco', 22, 3.7, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 23, 3.95, 2.3, -0.3, 2.8, 0.8) VALUES ('monaco', 23, 3.95, -2.3, 0.3, 2.8, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1')
@@ -108,93 +108,93 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27, VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27,
3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen', 3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen',
0.3, 0.4, 4.05, 2.7, 0.3, -2.7, 4.05, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 0, 0.3, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 0, 0.3, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 1, 1.4, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 1, 1.4, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 2, 1.7, 0.65, 1.5707963267948966, 2.2, 1.6) VALUES ('barcelona-catalunya', 2, 1.7, -0.65, -1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 3, 1.5, 0.95, 3.141592653589793, 2, 2) VALUES ('barcelona-catalunya', 3, 1.5, -0.95, -3.141592653589793, 2, 2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 4, 1.2, 1.1, 3.4415926535897934, 2.6, 1) VALUES ('barcelona-catalunya', 4, 1.2, -1.1, -3.4415926535897934, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 5, 0.95, 1, 4.0415926535897935, 2.6, 1) VALUES ('barcelona-catalunya', 5, 0.95, -1, -4.0415926535897935, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 6, 0.8, 1.2, -1.5707963267948966, 2.4, 1.2) VALUES ('barcelona-catalunya', 6, 0.8, -1.2, 1.5707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 7, 1.05, 1.5, 0, 3.2, 0.7) VALUES ('barcelona-catalunya', 7, 1.05, -1.5, -0, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 8, 1.3, 1.65, 1.5707963267948966, 2.4, 1) VALUES ('barcelona-catalunya', 8, 1.3, -1.65, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 9, 1.15, 1.85, 3.141592653589793, 2.4, 1) VALUES ('barcelona-catalunya', 9, 1.15, -1.85, -3.141592653589793, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 10, 1.4, 2, 2.641592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 10, 1.4, -2, -2.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 11, 1.7, 2, 2.141592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 11, 1.7, -2, -2.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 12, 1.95, 2.2, 1.5707963267948966, 2.8, 0.5) VALUES ('barcelona-catalunya', 12, 1.95, -2.2, -1.5707963267948966, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 13, 1.65, 2.5, 0.3, 2.8, 0.5) VALUES ('barcelona-catalunya', 13, 1.65, -2.5, -0.3, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 14, 2.2, 2.7, 0.7, 3.2, 0.5) VALUES ('barcelona-catalunya', 14, 2.2, -2.7, -0.7, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 15, 2.8, 2.7, 1.1, 3.4, 0.5) VALUES ('barcelona-catalunya', 15, 2.8, -2.7, -1.1, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 16, 3.1, 2.5, 1.9707963267948967, 2.2, 1.4) VALUES ('barcelona-catalunya', 16, 3.1, -2.5, -1.9707963267948967, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 17, 2.95, 2.2, 3.541592653589793, 2.2, 1.4) VALUES ('barcelona-catalunya', 17, 2.95, -2.2, -3.541592653589793, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 18, 3.3, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 18, 3.3, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 19, 4, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 19, 4, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 20, 4.05, 1.6, -1.5707963267948966, 2.4, 1.4) VALUES ('barcelona-catalunya', 20, 4.05, -1.6, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 21, 3.8, 1.4, 3.141592653589793, 2.4, 1.4) VALUES ('barcelona-catalunya', 21, 3.8, -1.4, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 22, 3.5, 1.5, 3.9269908169872414, 2.6, 1) VALUES ('barcelona-catalunya', 22, 3.5, -1.5, -3.9269908169872414, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 23, 3.1, 1.25, -1.9707963267948967, 3, 0.7) VALUES ('barcelona-catalunya', 23, 3.1, -1.25, 1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 24, 2.7, 1, -2.4707963267948965, 3, 0.7) VALUES ('barcelona-catalunya', 24, 2.7, -1, 2.4707963267948965, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 25, 2.3, 0.7, 3.541592653589793, 3.4, 0.5) VALUES ('barcelona-catalunya', 25, 2.3, -0.7, -3.541592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 26, 1.6, 0.5, 3.3415926535897933, 3.6, 0.4) VALUES ('barcelona-catalunya', 26, 1.6, -0.5, -3.3415926535897933, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 27, 0.9, 0.42, 0, 4, 0) VALUES ('barcelona-catalunya', 27, 0.9, -0.42, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1')
@@ -212,84 +212,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27, VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27,
3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen', 3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen',
0.3, 0.4, 3.85, 2.4, 0.3, -2.4, 3.85, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 0, 0.3, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 0, 0.3, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 1, 1.6, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 1, 1.6, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 2, 1.85, 0.65, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 2, 1.85, -0.65, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 3, 1.6, 0.95, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 3, 1.6, -0.95, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 4, 1.3, 1, 3.641592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 4, 1.3, -1, -3.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 5, 1.05, 1.2, -1.5707963267948966, 2.6, 1) VALUES ('austria-redbull-ring', 5, 1.05, -1.2, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 6, 1.4, 1.4, 0, 3.2, 0.5) VALUES ('austria-redbull-ring', 6, 1.4, -1.4, -0, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 7, 1.85, 1.4, 1.7707963267948965, 3.2, 0.5) VALUES ('austria-redbull-ring', 7, 1.85, -1.4, -1.7707963267948965, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 8, 2.1, 1.65, 3.141592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 8, 2.1, -1.65, -3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 9, 2, 1.85, -1.5707963267948966, 3, 0.7) VALUES ('austria-redbull-ring', 9, 2, -1.85, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 10, 2.25, 2.05, 0, 2, 1.8) VALUES ('austria-redbull-ring', 10, 2.25, -2.05, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 11, 2.4, 2, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 11, 2.4, -2, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 12, 2.4, 2.2, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 12, 2.4, -2.2, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 13, 2.2, 2.3, -1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 13, 2.2, -2.3, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 14, 2.55, 2.4, -0.2, 3.4, 0.4) VALUES ('austria-redbull-ring', 14, 2.55, -2.4, 0.2, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 15, 3, 2.4, 0, 4, 0) VALUES ('austria-redbull-ring', 15, 3, -2.4, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 16, 3.5, 2.4, 0, 4.5, 0) VALUES ('austria-redbull-ring', 16, 3.5, -2.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 17, 3.85, 2.15, 1.7707963267948965, 3.4, 0.6) VALUES ('austria-redbull-ring', 17, 3.85, -2.15, -1.7707963267948965, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 18, 3.6, 1.8, 3.141592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 18, 3.6, -1.8, -3.141592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 19, 3.25, 1.85, 3.741592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 19, 3.25, -1.85, -3.741592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 20, 3, 1.6, -1.9707963267948967, 3.2, 0.6) VALUES ('austria-redbull-ring', 20, 3, -1.6, 1.9707963267948967, 3.2, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 21, 2.6, 1.4, -3.141592653589793, 3.4, 0.5) VALUES ('austria-redbull-ring', 21, 2.6, -1.4, 3.141592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 22, 2, 1.2, 2.641592653589793, 3.6, 0.4) VALUES ('austria-redbull-ring', 22, 2, -1.2, -2.641592653589793, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 23, 1.4, 0.8, 2.9415926535897934, 3.8, 0.3) VALUES ('austria-redbull-ring', 23, 1.4, -0.8, -2.9415926535897934, 3.8, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 24, 0.8, 0.5, 0, 4.2, 0.2) VALUES ('austria-redbull-ring', 24, 0.8, -0.5, -0, 4.2, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1')
@@ -307,99 +307,99 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27, VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27,
3.5500000000000003, 2.4, 0.18, 'carpet', 0, 'Hamilton', 3.5500000000000003, 2.4, 0.18, 'carpet', 0, 'Hamilton',
0.3, 0.25, 3.85, 2.65, 0.3, -2.65, 3.85, -0.25,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 0, 0.3, 0.3, 0, 5, 0) VALUES ('great-britain-silverstone', 0, 0.3, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 1, 2.4, 0.3, 0, 5, 0) VALUES ('great-britain-silverstone', 1, 2.4, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 2, 2.8, 0.45, 1.5707963267948966, 3, 0.8) VALUES ('great-britain-silverstone', 2, 2.8, -0.45, -1.5707963267948966, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 3, 2.6, 0.8, 3.141592653589793, 3.4, 0.6) VALUES ('great-britain-silverstone', 3, 2.6, -0.8, -3.141592653589793, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 4, 2.95, 1.05, 2.0707963267948966, 3.2, 0.7) VALUES ('great-britain-silverstone', 4, 2.95, -1.05, -2.0707963267948966, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 5, 3.3, 1.3, 0, 2.2, 1.6) VALUES ('great-britain-silverstone', 5, 3.3, -1.3, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 6, 3.6, 1.15, -1.5707963267948966, 2.2, 1.6) VALUES ('great-britain-silverstone', 6, 3.6, -1.15, 1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 7, 3.6, 0.8, -3.141592653589793, 3, 0.7) VALUES ('great-britain-silverstone', 7, 3.6, -0.8, 3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 8, 3.85, 0.55, -1.5707963267948966, 2.6, 1) VALUES ('great-britain-silverstone', 8, 3.85, -0.55, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 9, 3.55, 0.25, 0, 2.4, 1) VALUES ('great-britain-silverstone', 9, 3.55, -0.25, -0, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 10, 3.25, 0.25, 1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 10, 3.25, -0.25, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 11, 3.05, 0.55, 1.8707963267948966, 3.4, 0.5) VALUES ('great-britain-silverstone', 11, 3.05, -0.55, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 12, 2.85, 0.45, 3.141592653589793, 4, 0.3) VALUES ('great-britain-silverstone', 12, 2.85, -0.45, -3.141592653589793, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 13, 1.8, 0.3, 0, 4.6, 0) VALUES ('great-britain-silverstone', 13, 1.8, -0.3, -0, 4.6, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 14, 1.2, 0.45, -1.7707963267948965, 3.8, 0.4) VALUES ('great-britain-silverstone', 14, 1.2, -0.45, 1.7707963267948965, 3.8, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 15, 1, 0.85, -3.541592653589793, 3.6, 0.5) VALUES ('great-britain-silverstone', 15, 1, -0.85, 3.541592653589793, 3.6, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 16, 0.7, 1.15, 1.8707963267948966, 2.4, 1.2) VALUES ('great-britain-silverstone', 16, 0.7, -1.15, -1.8707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 17, 0.5, 0.9, 0, 2.6, 0.9) VALUES ('great-britain-silverstone', 17, 0.5, -0.9, -0, 2.6, 0.9)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 18, 0.7, 0.65, -1.5707963267948966, 3, 0.7) VALUES ('great-britain-silverstone', 18, 0.7, -0.65, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 19, 0.45, 0.45, -3.541592653589793, 2.6, 1) VALUES ('great-britain-silverstone', 19, 0.45, -0.45, 3.541592653589793, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 20, 0.75, 1, 1.1707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 20, 0.75, -1, -1.1707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 21, 0.5, 1.5, -3.541592653589793, 2.6, 0.8) VALUES ('great-britain-silverstone', 21, 0.5, -1.5, 3.541592653589793, 2.6, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 22, 0.3, 1.15, -1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 22, 0.3, -1.15, 1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 23, 0.7, 1.6, 0.7853981633974483, 3.4, 0.5) VALUES ('great-britain-silverstone', 23, 0.7, -1.6, -0.7853981633974483, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 24, 2, 1.95, 1.9707963267948967, 3, 0.7) VALUES ('great-britain-silverstone', 24, 2, -1.95, -1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 25, 3, 2.4, 3.3415926535897933, 3.4, 0.4) VALUES ('great-britain-silverstone', 25, 3, -2.4, -3.3415926535897933, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 26, 2.4, 2.65, -1.8707963267948966, 4, 0.3) VALUES ('great-britain-silverstone', 26, 2.4, -2.65, 1.8707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 27, 1.4, 2.6, -3.141592653589793, 4.4, 0.2) VALUES ('great-britain-silverstone', 27, 1.4, -2.6, 3.141592653589793, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 28, 0.5, 2.3, -2.8415926535897933, 4.8, 0.2) VALUES ('great-britain-silverstone', 28, 0.5, -2.3, 2.8415926535897933, 4.8, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 29, 0.3, 1.7, -1.2707963267948965, 5, 0.1) VALUES ('great-britain-silverstone', 29, 0.3, -1.7, 1.2707963267948965, 5, 0.1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1')
@@ -417,84 +417,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27, VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27,
3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen', 3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen',
0.4, 0.5, 4.25, 2.75, 0.4, -2.75, 4.25, -0.5,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 0, 0.4, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 0, 0.4, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 1, 1.6, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 1, 1.6, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 2, 1.95, 0.6, 0.3, 4.2, 0.5) VALUES ('belgium-spa', 2, 1.95, -0.6, -0.3, 4.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 3, 2.2, 0.95, 0.9, 4, 0.6) VALUES ('belgium-spa', 3, 2.2, -0.95, -0.9, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 4, 2.1, 1.3, 3.541592653589793, 4, 0.6) VALUES ('belgium-spa', 4, 2.1, -1.3, -3.541592653589793, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 5, 2.7, 1.5, 0, 5, 0) VALUES ('belgium-spa', 5, 2.7, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 6, 3.5, 1.5, 0, 5, 0) VALUES ('belgium-spa', 6, 3.5, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 7, 3.85, 1.3, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 7, 3.85, -1.3, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 8, 3.6, 1.1, 3.4415926535897934, 2.6, 1.2) VALUES ('belgium-spa', 8, 3.6, -1.1, -3.4415926535897934, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 9, 3.3, 1.25, 4.0415926535897935, 2.6, 1.2) VALUES ('belgium-spa', 9, 3.3, -1.25, -4.0415926535897935, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 10, 3.5, 1.55, 1.5707963267948966, 3.6, 0.4) VALUES ('belgium-spa', 10, 3.5, -1.55, -1.5707963267948966, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 11, 3.25, 1.85, 0, 2.2, 1.6) VALUES ('belgium-spa', 11, 3.25, -1.85, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 12, 3.05, 1.7, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 12, 3.05, -1.7, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 13, 3.25, 1.5, 0, 3, 0.7) VALUES ('belgium-spa', 13, 3.25, -1.5, -0, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 14, 3.7, 1.65, 1.8707963267948966, 3.4, 0.5) VALUES ('belgium-spa', 14, 3.7, -1.65, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 15, 4.1, 2, 2.1707963267948966, 4, 0.3) VALUES ('belgium-spa', 15, 4.1, -2, -2.1707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 16, 4.25, 2.4, 3.541592653589793, 4.6, 0.3) VALUES ('belgium-spa', 16, 4.25, -2.4, -3.541592653589793, 4.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 17, 4, 2.7, -1.8707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 17, 4, -2.7, 1.8707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 18, 3.7, 2.55, 3.141592653589793, 2.4, 1.4) VALUES ('belgium-spa', 18, 3.7, -2.55, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 19, 3.55, 2.75, 3.8415926535897933, 2.4, 1.4) VALUES ('belgium-spa', 19, 3.55, -2.75, -3.8415926535897933, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 20, 3.05, 2.7, -1.5707963267948966, 2, 1.8) VALUES ('belgium-spa', 20, 3.05, -2.7, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 21, 2.85, 2.45, 0, 2, 1.8) VALUES ('belgium-spa', 21, 2.85, -2.45, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 22, 2, 2.2, 2.8415926535897933, 3.6, 0.3) VALUES ('belgium-spa', 22, 2, -2.2, -2.8415926535897933, 3.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 23, 1.2, 1.6, 2.541592653589793, 4, 0.2) VALUES ('belgium-spa', 23, 1.2, -1.6, -2.541592653589793, 4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 24, 0.6, 1.05, 2.8415926535897933, 4.4, 0.2) VALUES ('belgium-spa', 24, 0.6, -1.05, -2.8415926535897933, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1')
@@ -20,56 +20,56 @@ UPDATE tracks SET
WHERE id = 'monaco'; WHERE id = 'monaco';
-- New centerline (51 waypoints, scale_xy=0.016, v=0.632 m/s) -- New centerline (51 waypoints, scale_xy=0.016, v=0.632 m/s)
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, -1.0454, 1.4710, 0.632, -0.1988); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, 1.0454, -1.4710, 0.632, -0.1988);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, -0.5857, 1.3926, 0.632, -0.1787); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, 0.5857, -1.3926, 0.632, -0.1787);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, -0.1312, 1.3043, 0.632, -0.2544); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, 0.1312, -1.3043, 0.632, -0.2544);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, 0.3084, 1.1557, 0.632, -0.4431); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, -0.3084, -1.1557, 0.632, -0.4431);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, 0.7141, 0.8629, 0.632, -1.0875); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, -0.7141, -0.8629, 0.632, -1.0875);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, 0.9426, 0.1919, 0.632, -1.2844); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, -0.9426, -0.1919, 0.632, -1.2844);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, 0.8648, -0.1182, 0.632, -0.2237); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, -0.8648, 0.1182, 0.632, -0.2237);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, 0.8332, -0.0660, 0.632, 0.0475); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, -0.8332, 0.0660, 0.632, 0.0475);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, 0.8035, -0.0743, 0.632, 0.0756); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, -0.8035, 0.0743, 0.632, 0.0756);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, 0.7644, 0.0045, 0.632, 0.0806); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, -0.7644, -0.0045, 0.632, 0.0806);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, 0.8077, 0.0000, 0.632, -0.0425); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, -0.8077, -0.0000, 0.632, -0.0425);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, 0.7644, -0.0351, 0.632, 0.3650); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, -0.7644, 0.0351, 0.632, 0.3650);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, 0.7753, 0.3862, 0.632, 1.3887); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, -0.7753, -0.3862, 0.632, 1.3887);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, 1.0811, 1.2440, 0.632, 1.4550); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, -1.0811, -1.2440, 0.632, 1.4550);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, 1.5315, 1.5878, 0.632, -0.2144); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, -1.5315, -1.5878, 0.632, -0.2144);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, 1.9468, 1.0284, 0.632, -1.2681); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, -1.9468, -1.0284, 0.632, -1.2681);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, 2.2249, 0.6162, 0.632, -0.4763); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, -2.2249, -0.6162, 0.632, -0.4763);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, 2.4832, 0.5226, 0.632, -0.1756); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, -2.4832, -0.5226, 0.632, -0.1756);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, 2.6842, 0.4546, 0.632, -0.1494); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, -2.6842, -0.4546, 0.632, -0.1494);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, 2.6842, -1.2476, 0.632, -0.0225); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, -2.6842, 1.2476, 0.632, -0.0225);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, 2.3570, -1.2545, 0.632, 0.1775); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, -2.3570, 1.2545, 0.632, 0.1775);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, 1.9356, -0.5751, 0.632, 34.1925); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, -1.9356, 0.5751, 0.632, 34.1925);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, 2.2577, 1.0415, 0.632, -0.8400); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, -2.2577, -1.0415, 0.632, -0.8400);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, 2.4933, -0.0053, 0.632, -3.1975); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, -2.4933, 0.0053, 0.632, -3.1975);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, 2.2544, -1.4159, 0.632, -2.7456); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, -2.2544, 1.4159, 0.632, -2.7456);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, 1.8399, -2.1127, 0.632, -0.6488); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, -1.8399, 2.1127, 0.632, -0.6488);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, 1.4629, -2.2528, 0.632, -0.2769); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, -1.4629, 2.2528, 0.632, -0.2769);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, 1.1199, -2.3708, 0.632, -0.3044); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, -1.1199, 2.3708, 0.632, -0.3044);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, 0.8170, -2.5300, 0.632, -0.3825); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, -0.8170, 2.5300, 0.632, -0.3825);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, 0.5897, -2.7255, 0.632, -0.4019); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, -0.5897, 2.7255, 0.632, -0.4019);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, 0.4437, -2.9031, 0.632, -0.3538); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, -0.4437, 2.9031, 0.632, -0.3538);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, 0.3711, -3.0547, 0.632, -0.3044); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, -0.3711, 3.0547, 0.632, -0.3044);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, 0.3632, 3.0973, 0.632, -0.0975); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, -0.3632, -3.0973, 0.632, -0.0975);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, 0.4121, 3.1387, 0.632, -0.0394); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, -0.4121, -3.1387, 0.632, -0.0394);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, 0.3659, 3.0594, 0.632, -0.2375); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, -0.3659, -3.0594, 0.632, -0.2375);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, 0.4854, 2.9319, 0.632, -0.0950); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, -0.4854, -2.9319, 0.632, -0.0950);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, 0.5588, 2.9664, 0.632, -0.2500); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, -0.5588, -2.9664, 0.632, -0.2500);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, 0.6474, -3.1052, 0.632, 0.8825); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, -0.6474, 3.1052, 0.632, 0.8825);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, 0.5269, -2.4971, 0.632, 1.3600); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, -0.5269, 2.4971, 0.632, 1.3600);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, 0.1409, -1.9547, 0.632, 0.9481); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, -0.1409, 1.9547, 0.632, 0.9481);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, -0.3108, -1.5706, 0.632, 0.3788); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, 0.3108, 1.5706, 0.632, 0.3788);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, -0.7633, -1.6025, 0.632, -0.3425); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, 0.7633, 1.6025, 0.632, -0.3425);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, -1.2092, -1.8788, 0.632, -0.0725); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, 1.2092, 1.8788, 0.632, -0.0725);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, -1.6280, -1.6659, 0.632, 0.7706); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, 1.6280, 1.6659, 0.632, 0.7706);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, -2.0840, -1.2045, 0.632, 0.2162); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, 2.0840, 1.2045, 0.632, 0.2162);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, -2.4739, -1.5682, 0.632, -2.4475); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, 2.4739, 1.5682, 0.632, -2.4475);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, -2.6842, 3.0638, 0.632, -4.0544); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, 2.6842, -3.0638, 0.632, -4.0544);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, -2.4303, 2.0706, 0.632, -1.2500); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, 2.4303, -2.0706, 0.632, -1.2500);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, -1.9728, 1.6822, 0.632, -0.4706); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, 1.9728, -1.6822, 0.632, -0.4706);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, -1.5093, 1.5774, 0.632, -0.2275); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, 1.5093, -1.5774, 0.632, -0.2275);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, -1.0454, 1.4710, 0.632, -0.1988); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, 1.0454, -1.4710, 0.632, -0.1988);
COMMIT; COMMIT;
@@ -0,0 +1,11 @@
-- 014_track_calendar.sql — schema for F1-style active track scheduling.
CREATE TABLE IF NOT EXISTS track_calendar (
id SERIAL PRIMARY KEY,
track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
start_at TIMESTAMP WITH TIME ZONE NOT NULL,
end_at TIMESTAMP WITH TIME ZONE NOT NULL,
CHECK (start_at < end_at)
);
CREATE INDEX IF NOT EXISTS track_calendar_dates_idx ON track_calendar (start_at, end_at);
@@ -0,0 +1,8 @@
-- 015_driver_car_id.sql — move device_id to cars table and use car_id in lobby_drivers
--
-- Represents the currently selected physical ESP32 DEVICE_ID (0..127) for a car.
-- And represents the currently selected car ID for a driver in lobby_drivers.
ALTER TABLE cars ADD COLUMN IF NOT EXISTS device_id INT;
ALTER TABLE lobby_drivers ADD COLUMN IF NOT EXISTS car_id TEXT;
ALTER TABLE lobby_drivers DROP COLUMN IF EXISTS device_id;
+57 -8
View File
@@ -142,7 +142,7 @@ func (s *PgStore) loadCars(ctx context.Context) ([]catalog.CarMeta, error) {
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active, drive, top_speed_ms, color_hex, avatar_url, active,
total_distance_m, total_races, total_laps, best_lap_ms, total_distance_m, total_races, total_laps, best_lap_ms,
created_ms, updated_ms created_ms, updated_ms, device_id
FROM cars FROM cars
ORDER BY id`) ORDER BY id`)
if err != nil { if err != nil {
@@ -161,7 +161,7 @@ func (s *PgStore) loadCars(ctx context.Context) ([]catalog.CarMeta, error) {
&c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry, &c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry,
&c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active, &c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active,
&c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs, &c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs,
&c.CreatedMs, &c.UpdatedMs, &c.CreatedMs, &c.UpdatedMs, &c.DeviceID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -260,8 +260,8 @@ func (s *PgStore) UpsertCar(ctx context.Context, c catalog.CarMeta) error {
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active, drive, top_speed_ms, color_hex, avatar_url, active,
total_distance_m, total_races, total_laps, best_lap_ms, total_distance_m, total_races, total_laps, best_lap_ms,
created_ms, updated_ms) created_ms, updated_ms, device_id)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
ON CONFLICT (id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name, name = EXCLUDED.name,
owner_id = EXCLUDED.owner_id, owner_id = EXCLUDED.owner_id,
@@ -293,7 +293,8 @@ func (s *PgStore) UpsertCar(ctx context.Context, c catalog.CarMeta) error {
total_races = EXCLUDED.total_races, total_races = EXCLUDED.total_races,
total_laps = EXCLUDED.total_laps, total_laps = EXCLUDED.total_laps,
best_lap_ms = EXCLUDED.best_lap_ms, best_lap_ms = EXCLUDED.best_lap_ms,
updated_ms = EXCLUDED.updated_ms`, updated_ms = EXCLUDED.updated_ms,
device_id = EXCLUDED.device_id`,
c.ID, c.Name, c.OwnerID, string(c.Visibility), c.Scale, c.ID, c.Name, c.OwnerID, string(c.Visibility), c.Scale,
c.LengthMm, c.WidthMm, c.HeightMm, c.WeightG, c.LengthMm, c.WidthMm, c.HeightMm, c.WeightG,
c.Chassis.Model, c.Chassis.Material, c.Chassis.Printed, c.Chassis.Model, c.Chassis.Material, c.Chassis.Printed,
@@ -302,7 +303,7 @@ func (s *PgStore) UpsertCar(ctx context.Context, c catalog.CarMeta) error {
c.Battery.VoltageV, c.Battery.CapacityMah, c.Battery.Cells, c.Battery.Chemistry, c.Battery.VoltageV, c.Battery.CapacityMah, c.Battery.Cells, c.Battery.Chemistry,
string(c.Drive), c.TopSpeedMs, c.ColorHex, c.AvatarURL, c.Active, string(c.Drive), c.TopSpeedMs, c.ColorHex, c.AvatarURL, c.Active,
c.TotalDistanceM, c.TotalRaces, c.TotalLaps, c.BestLapMs, c.TotalDistanceM, c.TotalRaces, c.TotalLaps, c.BestLapMs,
c.CreatedMs, c.UpdatedMs, c.CreatedMs, c.UpdatedMs, c.DeviceID,
) )
return err return err
} }
@@ -333,7 +334,7 @@ func (s *PgStore) UpdateCarStats(ctx context.Context, id string, fn func(*catalo
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active, drive, top_speed_ms, color_hex, avatar_url, active,
total_distance_m, total_races, total_laps, best_lap_ms, total_distance_m, total_races, total_laps, best_lap_ms,
created_ms, updated_ms created_ms, updated_ms, device_id
FROM cars WHERE id = $1 FOR UPDATE`, id, FROM cars WHERE id = $1 FOR UPDATE`, id,
).Scan( ).Scan(
&c.ID, &c.Name, &c.OwnerID, &c.Visibility, &c.Scale, &c.ID, &c.Name, &c.OwnerID, &c.Visibility, &c.Scale,
@@ -344,7 +345,7 @@ func (s *PgStore) UpdateCarStats(ctx context.Context, id string, fn func(*catalo
&c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry, &c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry,
&c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active, &c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active,
&c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs, &c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs,
&c.CreatedMs, &c.UpdatedMs, &c.CreatedMs, &c.UpdatedMs, &c.DeviceID,
) )
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
@@ -371,3 +372,51 @@ func (s *PgStore) UpdateCarStats(ctx context.Context, id string, fn func(*catalo
} }
return tx.Commit(ctx) return tx.Commit(ctx)
} }
// GetTrackCalendar returns all track calendar entries ordered by start_at.
func (s *PgStore) GetTrackCalendar(ctx context.Context) ([]catalog.TrackCalendarEntry, error) {
query := `SELECT id, track_id, start_at, end_at FROM track_calendar ORDER BY start_at ASC`
rows, err := s.pool.Query(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []catalog.TrackCalendarEntry
for rows.Next() {
var entry catalog.TrackCalendarEntry
if err := rows.Scan(&entry.ID, &entry.TrackID, &entry.StartAt, &entry.EndAt); err != nil {
return nil, err
}
entries = append(entries, entry)
}
return entries, nil
}
// CreateTrackCalendar inserts a new calendar entry.
func (s *PgStore) CreateTrackCalendar(ctx context.Context, entry catalog.TrackCalendarEntry) (catalog.TrackCalendarEntry, error) {
query := `
INSERT INTO track_calendar (track_id, start_at, end_at)
VALUES ($1, $2, $3)
RETURNING id, track_id, start_at, end_at`
row := s.pool.QueryRow(ctx, query, entry.TrackID, entry.StartAt, entry.EndAt)
var res catalog.TrackCalendarEntry
if err := row.Scan(&res.ID, &res.TrackID, &res.StartAt, &res.EndAt); err != nil {
return catalog.TrackCalendarEntry{}, err
}
return res, nil
}
// DeleteTrackCalendar deletes a calendar entry by ID.
func (s *PgStore) DeleteTrackCalendar(ctx context.Context, id int) error {
command := `DELETE FROM track_calendar WHERE id = $1`
res, err := s.pool.Exec(ctx, command, id)
if err != nil {
return err
}
if res.RowsAffected() == 0 {
return catalog.ErrNotFound
}
return nil
}
+75 -1
View File
@@ -239,6 +239,19 @@ type VersionResponse struct {
Version string `json:"version" example:"0.1.0-poc"` Version string `json:"version" example:"0.1.0-poc"`
} }
// WebRTCConnectRequest is the body of the WebRTC connection API.
type WebRTCConnectRequest struct {
SDP string `json:"sdp"`
Type string `json:"type" example:"offer"`
DriverID string `json:"driver_id" example:"driver-alice"`
}
// WebRTCConnectResponse is the response to WebRTCConnectRequest.
type WebRTCConnectResponse struct {
SDP string `json:"sdp"`
Type string `json:"type" example:"answer"`
}
// LobbyDriver is the wire form of lobby.DriverMeta. // LobbyDriver is the wire form of lobby.DriverMeta.
type LobbyDriver struct { type LobbyDriver struct {
ID string `json:"id"` ID string `json:"id"`
@@ -249,7 +262,6 @@ type LobbyDriver struct {
LastSeenMs int64 `json:"last_seen_ms"` LastSeenMs int64 `json:"last_seen_ms"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
AvatarHue int `json:"avatar_hue,omitempty"` AvatarHue int `json:"avatar_hue,omitempty"`
DeviceID *int `json:"device_id,omitempty"`
} }
// LobbySnapshot is the full lobby state, broadcast on change. // LobbySnapshot is the full lobby state, broadcast on change.
@@ -350,6 +362,7 @@ type TrackWire struct {
BestLapHolder string `json:"best_lap_holder,omitempty"` BestLapHolder string `json:"best_lap_holder,omitempty"`
CreatedMs int64 `json:"created_ms"` CreatedMs int64 `json:"created_ms"`
UpdatedMs int64 `json:"updated_ms"` UpdatedMs int64 `json:"updated_ms"`
IsActive bool `json:"is_active" example:"true"`
} }
// TrackListRequest: payload { "scope": "all|system|public|private", "tag": "..." }. // TrackListRequest: payload { "scope": "all|system|public|private", "tag": "..." }.
@@ -465,6 +478,7 @@ type CarWire struct {
CreatedMs int64 `json:"created_ms"` CreatedMs int64 `json:"created_ms"`
UpdatedMs int64 `json:"updated_ms"` UpdatedMs int64 `json:"updated_ms"`
DeviceID *int `json:"device_id,omitempty"`
} }
// CarListRequest: payload { "scope": "all|system|public|private", "owner_id": "..." }. // CarListRequest: payload { "scope": "all|system|public|private", "owner_id": "..." }.
@@ -506,6 +520,7 @@ type CarPatch struct {
ColorHex *string `json:"color_hex,omitempty"` ColorHex *string `json:"color_hex,omitempty"`
AvatarURL *string `json:"avatar_url,omitempty"` AvatarURL *string `json:"avatar_url,omitempty"`
Active *bool `json:"active,omitempty"` Active *bool `json:"active,omitempty"`
DeviceID *int `json:"device_id,omitempty"`
} }
// CarDeleteRequest: payload { "id": "..." }. // CarDeleteRequest: payload { "id": "..." }.
@@ -827,6 +842,65 @@ type RaceResultMsg struct {
FinishedAtMs int64 `json:"finished_at_ms"` FinishedAtMs int64 `json:"finished_at_ms"`
} }
// LeaderboardEntry is a single row in the paginated leaderboard response.
type LeaderboardEntry struct {
DriverID string `json:"driver_id" example:"driver-seed-001"`
Nickname string `json:"nickname" example:"ACE"`
Name string `json:"name" example:"Alice"`
AvatarURL string `json:"avatar_url" example:"https://cdn.example.com/u/ace.png"`
ClanID string `json:"clan_id,omitempty" example:"clan-seed-001"`
ClanTag string `json:"clan_tag,omitempty" example:"ACE"`
Points int `json:"points" example:"25"`
BestPos *int `json:"best_pos,omitempty" example:"1"` // Only populated for track-specific leaderboard
BestTimeMs *int64 `json:"best_time_ms,omitempty" example:"15000"`
Rank int `json:"rank" example:"1"`
}
// LeaderboardResponse is the paginated response for GET /api/leaderboard.
type LeaderboardResponse struct {
Items []LeaderboardEntry `json:"items"`
Total int `json:"total" example:"8"`
Limit int `json:"limit" example:"50"`
Offset int `json:"offset" example:"0"`
CurrentDriver *LeaderboardEntry `json:"current_driver,omitempty"`
}
// TrackCalendarEntry is the wire representation of an active track schedule period.
type TrackCalendarEntry struct {
ID int `json:"id" example:"1"`
TrackID string `json:"track_id" example:"monaco"`
StartAt time.Time `json:"start_at" example:"2026-07-15T00:00:00Z"`
EndAt time.Time `json:"end_at" example:"2026-07-30T00:00:00Z"`
}
// TrackCalendarCreateRequest is the payload to schedule a track active period.
type TrackCalendarCreateRequest struct {
TrackID string `json:"track_id" example:"monaco"`
StartAt time.Time `json:"start_at" example:"2026-07-15T00:00:00Z"`
EndAt time.Time `json:"end_at" example:"2026-07-30T00:00:00Z"`
}
// TrackCalendarResponse is the wrapper for the calendar list.
type TrackCalendarResponse struct {
Items []TrackCalendarEntry `json:"items"`
}
// TrackCalendarLeaderboard holds the leaderboard summary for a track calendar event.
type TrackCalendarLeaderboard struct {
TrackID string `json:"track_id" example:"monaco"`
TrackName string `json:"track_name" example:"Monaco"`
StartAt time.Time `json:"start_at" example:"2026-07-15T00:00:00Z"`
EndAt time.Time `json:"end_at" example:"2026-07-30T00:00:00Z"`
Status string `json:"status" example:"active"`
Podium []LeaderboardEntry `json:"podium"`
CurrentDriver *LeaderboardEntry `json:"current_driver,omitempty"`
}
// TrackCalendarLeaderboardResponse is the list response for calendar leaderboards.
type TrackCalendarLeaderboardResponse struct {
Items []TrackCalendarLeaderboard `json:"items"`
}
// Encode marshals an envelope to JSON bytes. // Encode marshals an envelope to JSON bytes.
func Encode(env *Envelope) ([]byte, error) { func Encode(env *Envelope) ([]byte, error) {
if env.TSMs == 0 { if env.TSMs == 0 {
+29
View File
@@ -79,6 +79,35 @@
<div class="wheel"> <div class="wheel">
<div class="wheel-disc" id="wheelDisc"></div> <div class="wheel-disc" id="wheelDisc"></div>
</div> </div>
<div class="control-extra" style="display: flex; gap: 10px; margin-top: 15px; justify-content: space-around; border-top: 1px solid #333; padding-top: 12px;">
<div class="gear-indicator" style="text-align: center;">
<div style="font-size: 0.8rem; color: #aaa;">GEAR</div>
<div style="display: flex; align-items: center; gap: 5px; margin-top: 4px;">
<button id="btnGearDown" class="btn" style="padding: 2px 8px; font-size: 0.8rem;">-</button>
<b id="lblGear" style="font-size: 1.3rem; min-width: 15px; display: inline-block;">N</b>
<button id="btnGearUp" class="btn" style="padding: 2px 8px; font-size: 0.8rem;">+</button>
</div>
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(Z / X)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #aaa;">DRS</div>
<button id="btnDrs" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px;">Toggle</button>
<div id="lblDrs" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ff3b30;">OFF</div>
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(Q)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #aaa;">PIT LIMIT</div>
<button id="btnPit" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px;">Toggle</button>
<div id="lblPit" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ff3b30;">OFF</div>
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(E)</div>
</div>
</div>
<div style="margin-top: 15px; border-top: 1px solid #333; padding-top: 12px;">
<span style="font-size: 0.8rem; color: #aaa;">Пакет управления (JSON):</span>
<pre id="ctrlJson" style="font-family: monospace; font-size: 0.75rem; background: #111; border: 1px solid #222; padding: 8px; border-radius: 4px; color: #00ff66; margin: 6px 0 0 0; overflow-x: auto; max-height: 250px; text-align: left; line-height: 1.2;">{}</pre>
</div>
</div> </div>
<div class="card"> <div class="card">
+100 -1
View File
@@ -35,6 +35,14 @@
tPos: $('tPos'), tPos: $('tPos'),
tTsm: $('tTsm'), tTsm: $('tTsm'),
tCars: $('tCars'), tCars: $('tCars'),
lblGear: $('lblGear'),
lblDrs: $('lblDrs'),
lblPit: $('lblPit'),
btnDrs: $('btnDrs'),
btnPit: $('btnPit'),
btnGearUp: $('btnGearUp'),
btnGearDown: $('btnGearDown'),
ctrlJson: $('ctrlJson'),
}; };
// -------- State -------- // -------- State --------
@@ -53,7 +61,7 @@
snapshotCount: 0, snapshotCount: 0,
snapshotWindow: [], snapshotWindow: [],
keys: new Set(), keys: new Set(),
input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0 }, input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0, gear: 0, drs: false, pitLimiter: false },
pingTimer: null, pingTimer: null,
inputTimer: null, inputTimer: null,
slotHint: 0, // сервер выдаёт слот; 0 по умолчанию slotHint: 0, // сервер выдаёт слот; 0 по умолчанию
@@ -113,6 +121,12 @@
state.input.steer = steer; state.input.steer = steer;
state.input.handbrake = handbrake; state.input.handbrake = handbrake;
// Calculate buttons bitmask: bit 0 (DRS), bit 1 (Pit Limiter)
let buttons = 0;
if (state.input.drs) buttons |= 1;
if (state.input.pitLimiter) buttons |= 2;
state.input.buttons = buttons;
// Update UI bars // Update UI bars
ui.lblThrottle.textContent = throttle.toFixed(2); ui.lblThrottle.textContent = throttle.toFixed(2);
ui.lblBrake.textContent = brake.toFixed(2); ui.lblBrake.textContent = brake.toFixed(2);
@@ -129,6 +143,48 @@
ui.barSteer.style.width = '2%'; ui.barSteer.style.width = '2%';
ui.wheelDisc.style.transform = `rotate(${steer * 90}deg)`; ui.wheelDisc.style.transform = `rotate(${steer * 90}deg)`;
// Update Gear, DRS, Pit Limiter UI text & class if elements exist
if (ui.lblGear) {
let gName = 'N';
if (state.input.gear === -1) gName = 'R';
if (state.input.gear === 1) gName = 'D';
ui.lblGear.textContent = gName;
}
if (ui.lblDrs) {
ui.lblDrs.textContent = state.input.drs ? 'ON' : 'OFF';
ui.lblDrs.className = state.input.drs ? 'val active' : 'val';
}
if (ui.lblPit) {
ui.lblPit.textContent = state.input.pitLimiter ? 'ON' : 'OFF';
ui.lblPit.className = state.input.pitLimiter ? 'val active' : 'val';
}
// Format control packet as JSON and show in UI
if (ui.ctrlJson) {
const steerByte = Math.round((steer + 1.0) * 100);
const throttleByte = Math.round(throttle * 100);
const brakeByte = Math.round(brake * 100);
const gearByte = state.input.gear === -1 ? 0 : (state.input.gear === 1 ? 2 : 1);
const jsonStr = JSON.stringify({
sent_json: {
steering: steer,
throttle: throttle,
brake: brake,
gear: state.input.gear,
buttons: buttons
},
binary_packet_bytes: {
byte0_steer: `0x${steerByte.toString(16).toUpperCase().padStart(2, '0')} (${steerByte})`,
byte1_throttle: `0x${throttleByte.toString(16).toUpperCase().padStart(2, '0')} (${throttleByte}%)`,
byte2_brake: `0x${brakeByte.toString(16).toUpperCase().padStart(2, '0')} (${brakeByte}%)`,
byte3_gear: `0x${gearByte.toString(16).toUpperCase().padStart(2, '0')} (${gearByte === 0 ? 'R' : (gearByte === 2 ? 'D' : 'N')})`,
byte4_flags: `0x${buttons.toString(16).toUpperCase().padStart(2, '0')} (DRS=${state.input.drs ? 1 : 0}, Pit=${state.input.pitLimiter ? 1 : 0})`
}
}, null, 2);
ui.ctrlJson.textContent = jsonStr;
}
} }
// -------- Send helpers -------- // -------- Send helpers --------
@@ -450,6 +506,24 @@
if (tag === 'INPUT' || tag === 'TEXTAREA') return; if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault(); if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
state.keys.add(k); state.keys.add(k);
// Toggle DRS, Pit Limiter, and shift gears
if (k === 'q') {
state.input.drs = !state.input.drs;
pollInput();
}
if (k === 'e') {
state.input.pitLimiter = !state.input.pitLimiter;
pollInput();
}
if (k === 'z') {
state.input.gear = Math.max(-1, state.input.gear - 1);
pollInput();
}
if (k === 'x') {
state.input.gear = Math.min(1, state.input.gear + 1);
pollInput();
}
}); });
window.addEventListener('keyup', (e) => { window.addEventListener('keyup', (e) => {
state.keys.delete(keyName(e)); state.keys.delete(keyName(e));
@@ -462,6 +536,31 @@
ui.btnJoin.addEventListener('click', joinRace); ui.btnJoin.addEventListener('click', joinRace);
ui.btnLeave.addEventListener('click', leaveRace); ui.btnLeave.addEventListener('click', leaveRace);
if (ui.btnDrs) {
ui.btnDrs.addEventListener('click', () => {
state.input.drs = !state.input.drs;
pollInput();
});
}
if (ui.btnPit) {
ui.btnPit.addEventListener('click', () => {
state.input.pitLimiter = !state.input.pitLimiter;
pollInput();
});
}
if (ui.btnGearUp) {
ui.btnGearUp.addEventListener('click', () => {
state.input.gear = Math.min(1, state.input.gear + 1);
pollInput();
});
}
if (ui.btnGearDown) {
ui.btnGearDown.addEventListener('click', () => {
state.input.gear = Math.max(-1, state.input.gear - 1);
pollInput();
});
}
// -------- Boot -------- // -------- Boot --------
setConn('disconnected', ''); setConn('disconnected', '');
log('x0gp PoC client ready. Press Connect.', 'meta'); log('x0gp PoC client ready. Press Connect.', 'meta');
+395
View File
@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>x0gp — Race Track Viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Plus+Jakarta+Sans:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0f19;
--card-bg: rgba(17, 24, 39, 0.7);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--accent: #3b82f6;
--accent-glow: rgba(59, 130, 246, 0.15);
--gradient-accent: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
min-height: 100vh;
padding: 2.5rem;
background-image:
radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.05) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(37, 99, 235, 0.05) 0%, transparent 40%);
background-attachment: fixed;
}
header {
max-width: 1200px;
margin: 0 auto 3rem auto;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-family: 'Outfit', sans-serif;
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #ffffff 0%, #a5b4fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: var(--text-secondary);
font-size: 0.95rem;
margin-top: 0.25rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
backdrop-filter: blur(12px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(800px circle at var(--x, 0px) var(--y, 0px), rgba(255, 255, 255, 0.06), transparent 40%);
pointer-events: none;
opacity: 0;
transition: opacity 0.5s;
}
.card:hover::before {
opacity: 1;
}
.card:hover {
transform: translateY(-4px);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(59, 130, 246, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.track-name {
font-family: 'Outfit', sans-serif;
font-size: 1.35rem;
font-weight: 600;
color: #ffffff;
}
.track-id {
font-family: monospace;
font-size: 0.8rem;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.05);
padding: 0.2rem 0.5rem;
border-radius: 6px;
}
.canvas-container {
width: 100%;
height: 280px;
background-color: #0b0f19;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.03);
position: relative;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
font-size: 0.85rem;
}
.stat-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.03);
padding: 0.5rem 0.75rem;
border-radius: 8px;
display: flex;
justify-content: space-between;
}
.stat-label {
color: var(--text-secondary);
}
.stat-value {
color: #ffffff;
font-weight: 600;
}
.tags-container {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.tag {
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 20px;
background: rgba(59, 130, 246, 0.1);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.2);
}
.tag-active {
background: rgba(16, 185, 129, 0.1);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.loader {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
width: 100%;
grid-column: 1 / -1;
flex-direction: column;
gap: 1rem;
color: var(--text-secondary);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(59, 130, 246, 0.1);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<header>
<div>
<h1>x0gp — Race Track Catalog</h1>
<div class="subtitle">Visualizing physical tracks and their generated centerline geometry</div>
</div>
</header>
<main class="grid" id="tracks-grid">
<div class="loader">
<div class="spinner"></div>
<div>Loading tracks from server API...</div>
</div>
</main>
<script>
// Track SVG centerline rendering algorithm
function generateSvgPath(centerline, bounds, padding = 30, size = 260) {
if (!centerline || centerline.length < 2) return null;
const minX = bounds.min_x;
const maxX = bounds.max_x;
const minY = bounds.min_y;
const maxY = bounds.max_y;
const rangeX = maxX - minX || 1;
const rangeY = maxY - minY || 1;
const aspectRatio = rangeX / rangeY;
let width, height;
if (aspectRatio > 1) {
width = size;
height = size / aspectRatio;
} else {
width = size * aspectRatio;
height = size;
}
const canvasWidth = width + padding * 2;
const canvasHeight = height + padding * 2;
const pixelPoints = centerline.map((point) => ({
x: ((point.x - minX) / rangeX) * width + padding,
y: ((point.y - minY) / rangeY) * height + padding,
heading: point.heading_rad,
curvature: point.curvature
}));
let dAttribute = '';
for (let i = 0; i < pixelPoints.length; i++) {
const current = pixelPoints[i];
if (i === 0) {
dAttribute += `M ${current.x.toFixed(2)} ${current.y.toFixed(2)}`;
} else {
const prev = pixelPoints[i - 1];
const dx = current.x - prev.x;
const dy = current.y - prev.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const tension = 0.30 + (prev.curvature * 0.05);
const cpLength = distance * tension;
const cp1x = prev.x + Math.cos(prev.heading) * cpLength;
const cp1y = prev.y + Math.sin(prev.heading) * cpLength;
const cp2x = current.x - Math.cos(current.heading) * cpLength;
const cp2y = current.y - Math.sin(current.heading) * cpLength;
dAttribute += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${current.x.toFixed(2)} ${current.y.toFixed(2)}`;
}
}
if (pixelPoints.length > 2) {
const first = pixelPoints[0];
const last = pixelPoints[pixelPoints.length - 1];
const dx = first.x - last.x;
const dy = first.y - last.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const tension = 0.3 + last.curvature * 0.05;
const cpLength = distance * tension;
const cp1x = last.x + Math.cos(last.heading) * cpLength;
const cp1y = last.y + Math.sin(last.heading) * cpLength;
const cp2x = first.x - Math.cos(first.heading) * cpLength;
const cp2y = first.y - Math.sin(first.heading) * cpLength;
dAttribute += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${first.x.toFixed(2)} ${first.y.toFixed(2)} Z`;
}
return {
d: dAttribute,
canvasWidth,
canvasHeight
};
}
async function init() {
const grid = document.getElementById('tracks-grid');
try {
const res = await fetch('/api/tracks');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
grid.innerHTML = '';
if (!data.tracks || data.tracks.length === 0) {
grid.innerHTML = '<div class="loader">No tracks found on the server.</div>';
return;
}
data.tracks.forEach(track => {
const card = document.createElement('div');
card.className = 'card';
// Track hover mouse light effect
card.addEventListener('mousemove', e => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
card.style.setProperty('--x', `${x}px`);
card.style.setProperty('--y', `${y}px`);
});
// Generate SVG
const svgData = generateSvgPath(track.centerline, track.bounds);
let svgHtml = '<div style="color: var(--text-secondary); font-size: 0.85rem;">No centerline coordinates</div>';
if (svgData) {
svgHtml = `
<svg width="100%" height="100%" viewBox="0 0 ${svgData.canvasWidth} ${svgData.canvasHeight}" preserveAspectRatio="xMidYMid meet" style="display: block; max-width: 100%; max-height: 100%; height: auto; transform-origin: center;">
<path d="${svgData.d}" fill="none" stroke="#ffffff" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" />
<path d="${svgData.d}" fill="none" stroke="#111827" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
</svg>
`;
}
const isActiveTag = track.is_active ? '<span class="tag tag-active">Active Track</span>' : '';
const bestLapStr = track.best_lap_ms > 0 ? `${(track.best_lap_ms / 1000).toFixed(3)}s` : 'None';
card.innerHTML = `
<div class="card-header">
<div>
<div class="track-name">${track.name}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 0.2rem;">${track.description || 'No description'}</div>
</div>
<div class="track-id">${track.id}</div>
</div>
<div class="canvas-container">
${svgHtml}
</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Length</span>
<span class="stat-value">${track.length_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Width</span>
<span class="stat-value">${track.width_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Lane Width</span>
<span class="stat-value">${track.lane_width_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Best Lap</span>
<span class="stat-value">${bestLapStr}</span>
</div>
</div>
<div class="tags-container">
<span class="tag" style="background: rgba(255,255,255,0.05); color: #fff; border-color: rgba(255,255,255,0.1); text-transform: capitalize;">${track.surface}</span>
${isActiveTag}
</div>
`;
grid.appendChild(card);
});
} catch (err) {
grid.innerHTML = `<div class="loader" style="color: #ef4444;">Failed to load tracks: ${err.message}</div>`;
}
}
init();
</script>
</body>
</html>
+876
View File
@@ -0,0 +1,876 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>x0gp · WebRTC Cockpit</title>
<!-- Modern typography -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0f19;
--panel-bg: rgba(17, 24, 39, 0.7);
--border-color: rgba(255, 255, 255, 0.08);
--primary: #10b981; /* Emerald */
--primary-hover: #059669;
--accent: #3b82f6; /* Blue */
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--danger: #ef4444;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Outfit', sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
background-image:
radial-gradient(circle at 10% 20%, rgba(16, 185, 129, 0.05) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.05) 0%, transparent 40%);
}
header {
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
backdrop-filter: blur(10px);
z-index: 10;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 800;
font-size: 1.5rem;
letter-spacing: -0.05em;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-badge {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
padding: 0.375rem 0.75rem;
border-radius: 9999px;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--danger);
box-shadow: 0 0 8px var(--danger);
transition: all 0.3s ease;
}
.status-dot.connected {
background-color: var(--primary);
box-shadow: 0 0 8px var(--primary);
}
.status-dot.connecting {
background-color: #f59e0b;
box-shadow: 0 0 8px #f59e0b;
animation: pulse 1s infinite alternate;
}
@keyframes pulse {
from { opacity: 0.5; }
to { opacity: 1; }
}
main {
flex: 1;
padding: 2rem;
max-width: 1400px;
width: 100%;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
@media (min-width: 1024px) {
main {
grid-template-columns: 1.6fr 1fr;
}
}
.glass-panel {
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 1.5rem;
backdrop-filter: blur(16px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
transition: border-color 0.3s ease;
}
.glass-panel:hover {
border-color: rgba(255, 255, 255, 0.12);
}
h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-main);
}
.auth-bar {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
input {
flex: 1;
background: rgba(0, 0, 0, 0.25);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem 1rem;
color: var(--text-main);
font-family: inherit;
font-size: 0.95rem;
transition: all 0.2s ease;
}
input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.15);
}
button {
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
border: none;
border-radius: 8px;
padding: 0.75rem 1.5rem;
color: white;
font-family: inherit;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.3);
}
button:active {
transform: translateY(0);
}
button.disconnect {
background: linear-gradient(135deg, var(--danger), #dc2626);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
}
.video-container {
position: relative;
width: 100%;
aspect-ratio: 4/3;
background: #020617;
border-radius: 12px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.04);
box-shadow: inset 0 0 40px rgba(0,0,0,0.8);
}
.video-feed {
width: 100%;
height: 100%;
object-fit: contain;
display: none;
}
.video-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
color: var(--text-muted);
text-align: center;
padding: 2rem;
}
.video-placeholder svg {
width: 48px;
height: 48px;
stroke: var(--text-muted);
opacity: 0.6;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.telemetry-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.telemetry-card {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 1rem;
text-align: center;
}
.telemetry-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 0.25rem;
}
.telemetry-value {
font-size: 1.5rem;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
color: var(--primary);
}
.controller-viz {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.steering-wheel-container {
position: relative;
width: 120px;
height: 120px;
}
.steering-wheel {
width: 100%;
height: 100%;
transition: transform 0.1s ease-out;
transform-origin: center center;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.5));
}
.keys-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
width: 160px;
}
.key-btn {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
border-radius: 8px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
transition: all 0.1s ease;
user-select: none;
}
.key-btn.active {
background: var(--primary);
color: #0b0f19;
box-shadow: 0 0 12px var(--primary);
border-color: var(--primary);
}
.key-btn.empty {
opacity: 0;
pointer-events: none;
}
.console {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 1rem;
height: 180px;
overflow-y: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: #10b981;
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
}
.console-entry {
margin-bottom: 0.35rem;
line-height: 1.3;
}
.console-time {
color: var(--text-muted);
}
.console-in {
color: var(--accent);
}
.console-out {
color: var(--primary);
}
footer {
padding: 1.5rem;
text-align: center;
font-size: 0.85rem;
color: var(--text-muted);
border-top: 1px solid var(--border-color);
margin-top: auto;
}
</style>
</head>
<body>
<header>
<div class="logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg>
x0gp <span style="font-weight: 300;">Cockpit</span>
</div>
<div class="status-badge">
<div id="statusDot" class="status-dot"></div>
<span id="statusText">Disconnected</span>
</div>
</header>
<main>
<!-- Left: Video Feed Panel -->
<div class="glass-panel" style="display: flex; flex-direction: column;">
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
Real-Time Video Feed
</h2>
<div class="video-container">
<img id="videoFeed" class="video-feed" alt="WebRTC Stream">
<div id="videoPlaceholder" class="video-placeholder">
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.182 16.318A4.486 4.486 0 0012.016 15a4.486 4.486 0 00-3.198 1.318M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z"/></svg>
<p>Connect the cockpit to receive real-time video</p>
</div>
</div>
<div class="controller-viz">
<div class="steering-wheel-container">
<!-- Steering wheel SVG -->
<svg id="steeringWheel" class="steering-wheel" viewBox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="45" stroke="#f3f4f6" stroke-width="6"/>
<circle cx="50" cy="50" r="38" stroke="var(--primary)" stroke-width="2" stroke-dasharray="5 5"/>
<rect x="15" y="47" width="70" height="6" rx="3" fill="#f3f4f6"/>
<rect x="47" y="50" width="6" height="35" rx="3" fill="#f3f4f6"/>
<circle cx="50" cy="50" r="10" fill="#0b0f19" stroke="#f3f4f6" stroke-width="3"/>
</svg>
</div>
<div class="keys-grid">
<div class="key-btn empty"></div>
<div id="keyW" class="key-btn">W</div>
<div class="key-btn empty"></div>
<div id="keyA" class="key-btn">A</div>
<div id="keyS" class="key-btn">S</div>
<div id="keyD" class="key-btn">D</div>
</div>
<div class="control-extra" style="display: flex; gap: 10px; margin-top: 15px; justify-content: space-around; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; width: 100%;">
<div class="gear-indicator" style="text-align: center;">
<div style="font-size: 0.8rem; color: #8a8d9a;">GEAR</div>
<div style="display: flex; align-items: center; gap: 5px; margin-top: 4px;">
<button id="btnGearDown" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">-</button>
<b id="lblGear" style="font-size: 1.3rem; min-width: 15px; display: inline-block;">N</b>
<button id="btnGearUp" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">+</button>
</div>
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(Z / X)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #8a8d9a;">DRS</div>
<button id="btnDrs" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">Toggle</button>
<div id="lblDrs" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ef4444;">OFF</div>
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(Q)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #8a8d9a;">PIT LIMIT</div>
<button id="btnPit" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">Toggle</button>
<div id="lblPit" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ef4444;">OFF</div>
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(E)</div>
</div>
</div>
<div style="margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; width: 100%;">
<span style="font-size: 0.8rem; color: #8a8d9a;">Пакет управления (JSON):</span>
<pre id="ctrlJson" style="font-family: monospace; font-size: 0.75rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.05); padding: 8px; border-radius: 4px; color: #00ff66; margin: 6px 0 0 0; overflow-x: auto; max-height: 250px; text-align: left; line-height: 1.2;">{}</pre>
</div>
</div>
</div>
<!-- Right: Telemetry & Controls Panel -->
<div class="glass-panel" style="display: flex; flex-direction: column; gap: 1.5rem;">
<div>
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Session Control
</h2>
<div class="auth-bar">
<input type="text" id="driverIdInput" placeholder="Enter Driver ID (e.g. ACE)" value="ACE">
<button id="connectBtn">Connect</button>
</div>
</div>
<div>
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 002 2h2a2 2 0 002-2z"/></svg>
Real-time Telemetry
</h2>
<div class="telemetry-grid">
<div class="telemetry-card">
<div class="telemetry-label">Speed</div>
<div id="telSpeed" class="telemetry-value">0.00 <span style="font-size: 0.8rem; font-weight: normal;">m/s</span></div>
</div>
<div class="telemetry-card">
<div class="telemetry-label">Ping Latency</div>
<div id="telPing" class="telemetry-value">0 <span style="font-size: 0.8rem; font-weight: normal;">ms</span></div>
</div>
<div class="telemetry-card">
<div class="telemetry-label">Car ID</div>
<div id="telDevice" class="telemetry-value">-</div>
</div>
<div class="telemetry-card">
<div class="telemetry-label">Position (X, Y)</div>
<div id="telPos" class="telemetry-value">0.0, 0.0</div>
</div>
</div>
</div>
<div style="flex: 1; display: flex; flex-direction: column;">
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
Console Log
</h2>
<div id="consoleLog" class="console">
<div class="console-entry"><span class="console-time">[18:58:00]</span> Welcome to x0gp Cockpit. Enter Driver ID and click Connect to initialize WebRTC.</div>
</div>
</div>
</div>
</main>
<footer>
x0gp · Pure Go WebRTC 1-to-1 Proxy System
</footer>
<script>
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const driverIdInput = document.getElementById('driverIdInput');
const connectBtn = document.getElementById('connectBtn');
const videoFeed = document.getElementById('videoFeed');
const videoPlaceholder = document.getElementById('videoPlaceholder');
const consoleLog = document.getElementById('consoleLog');
const steeringWheel = document.getElementById('steeringWheel');
// Telemetry DOM
const telSpeed = document.getElementById('telSpeed');
const telPing = document.getElementById('telPing');
const telDevice = document.getElementById('telDevice');
const telPos = document.getElementById('telPos');
let pc = null;
let dataChannel = null;
let videoChannel = null;
let sendControlsInterval = null;
let activeKeys = { w: false, a: false, s: false, d: false };
let currentSteer = 0; // -1 to 1
let currentThrottle = 0; // 0 to 1
let currentBrake = 0; // 0 to 1
let currentGear = 0; // 0: N, -1: R, 1: D
let drs = false;
let pitLimiter = false;
let buttons = 0;
let pingsReceived = 0;
let latencyStart = 0;
function log(message, type = 'system') {
const time = new Date().toTimeString().split(' ')[0];
const div = document.createElement('div');
div.className = 'console-entry';
let prefix = '';
if (type === 'in') prefix = ' <span class="console-in">&lt;-</span> ';
if (type === 'out') prefix = ' <span class="console-out">-&gt;</span> ';
div.innerHTML = `<span class="console-time">[${time}]</span>${prefix}${message}`;
consoleLog.appendChild(div);
consoleLog.scrollTop = consoleLog.scrollHeight;
}
// Keyboard event listeners
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (activeKeys.hasOwnProperty(key)) {
activeKeys[key] = true;
updateKeyVisuals();
}
if (key === 'q') {
drs = !drs;
updateKeyVisuals();
}
if (key === 'e') {
pitLimiter = !pitLimiter;
updateKeyVisuals();
}
if (key === 'z') {
currentGear = Math.max(-1, currentGear - 1);
updateKeyVisuals();
}
if (key === 'x') {
currentGear = Math.min(1, currentGear + 1);
updateKeyVisuals();
}
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (activeKeys.hasOwnProperty(key)) {
activeKeys[key] = false;
updateKeyVisuals();
}
});
function updateKeyVisuals() {
document.getElementById('keyW').classList.toggle('active', activeKeys.w);
document.getElementById('keyA').classList.toggle('active', activeKeys.a);
document.getElementById('keyS').classList.toggle('active', activeKeys.s);
document.getElementById('keyD').classList.toggle('active', activeKeys.d);
// Calculate wheel rotation based on keys
let targetRotation = 0;
if (activeKeys.a) targetRotation = -45;
if (activeKeys.d) targetRotation = 45;
steeringWheel.style.transform = `rotate(${targetRotation}deg)`;
// Compute values
currentThrottle = activeKeys.w ? 1.0 : 0.0;
currentBrake = activeKeys.s ? 1.0 : 0.0;
currentSteer = 0.0;
if (activeKeys.a) currentSteer = -1.0;
if (activeKeys.d) currentSteer = 1.0;
buttons = 0;
if (drs) buttons |= 1;
if (pitLimiter) buttons |= 2;
// Update UI Gear/DRS/Pit if elements exist
const lblGear = document.getElementById('lblGear');
if (lblGear) {
let gName = 'N';
if (currentGear === -1) gName = 'R';
if (currentGear === 1) gName = 'D';
lblGear.innerText = gName;
}
const lblDrs = document.getElementById('lblDrs');
if (lblDrs) {
lblDrs.innerText = drs ? 'ON' : 'OFF';
lblDrs.style.color = drs ? '#00ff66' : '#ef4444';
}
const lblPit = document.getElementById('lblPit');
if (lblPit) {
lblPit.innerText = pitLimiter ? 'ON' : 'OFF';
lblPit.style.color = pitLimiter ? '#00ff66' : '#ef4444';
}
const ctrlJson = document.getElementById('ctrlJson');
if (ctrlJson) {
const steerByte = Math.round((currentSteer + 1.0) * 100);
const throttleByte = Math.round(currentThrottle * 100);
const brakeByte = Math.round(currentBrake * 100);
const gearByte = currentGear === -1 ? 0 : (currentGear === 1 ? 2 : 1);
ctrlJson.innerText = JSON.stringify({
sent_json: {
steering: currentSteer,
throttle: currentThrottle,
brake: currentBrake,
gear: currentGear,
buttons: buttons
},
binary_packet_bytes: {
byte0_steer: `0x${steerByte.toString(16).toUpperCase().padStart(2, '0')} (${steerByte})`,
byte1_throttle: `0x${throttleByte.toString(16).toUpperCase().padStart(2, '0')} (${throttleByte}%)`,
byte2_brake: `0x${brakeByte.toString(16).toUpperCase().padStart(2, '0')} (${brakeByte}%)`,
byte3_gear: `0x${gearByte.toString(16).toUpperCase().padStart(2, '0')} (${gearByte === 0 ? 'R' : (gearByte === 2 ? 'D' : 'N')})`,
byte4_flags: `0x${buttons.toString(16).toUpperCase().padStart(2, '0')} (DRS=${drs ? 1 : 0}, Pit=${pitLimiter ? 1 : 0})`
}
}, null, 2);
}
// Log control changes to UI console
log(`Controls updated: throttle=${currentThrottle}, steering=${currentSteer}, brake=${currentBrake}, gear=${currentGear}, buttons=${buttons}`, 'out');
}
// Click listeners for the custom controls
document.getElementById('btnGearDown').addEventListener('click', () => {
currentGear = Math.max(-1, currentGear - 1);
updateKeyVisuals();
});
document.getElementById('btnGearUp').addEventListener('click', () => {
currentGear = Math.min(1, currentGear + 1);
updateKeyVisuals();
});
document.getElementById('btnDrs').addEventListener('click', () => {
drs = !drs;
updateKeyVisuals();
});
document.getElementById('btnPit').addEventListener('click', () => {
pitLimiter = !pitLimiter;
updateKeyVisuals();
});
connectBtn.addEventListener('click', () => {
if (pc) {
disconnectCockpit();
} else {
connectCockpit();
}
});
async function connectCockpit() {
const driverId = driverIdInput.value.trim().toUpperCase();
if (!driverId) {
alert('Please enter a valid Driver ID');
return;
}
log(`Initializing WebRTC Connection for Driver: ${driverId}...`);
updateStatus('connecting', 'Connecting...');
connectBtn.disabled = true;
try {
// 1. Create PeerConnection
pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 2. Create Data Channels (Client Initiated)
dataChannel = pc.createDataChannel('data', { ordered: true });
videoChannel = pc.createDataChannel('video', { ordered: false });
setupDataChannel(dataChannel);
setupVideoChannel(videoChannel);
// 3. Create SDP Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 4. Wait for ICE gathering to complete (Vanilla ICE)
log('Gathering ICE Candidates...');
await new Promise((resolve) => {
if (pc.iceGatheringState === 'complete') {
resolve();
} else {
function checkState() {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}
}
pc.addEventListener('icegatheringstatechange', checkState);
}
});
// 5. Post SDP Offer to Server
const signalingUrl = `${window.location.protocol}//localhost:8080/api/webrtc/connect`;
log(`Sending SDP Offer to ${signalingUrl}...`);
const response = await fetch(signalingUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sdp: pc.localDescription.sdp,
type: 'offer',
driver_id: driverId
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const answer = await response.json();
log('SDP Answer received from server. Establishing WebRTC channel...');
await pc.setRemoteDescription(new RTCSessionDescription(answer));
} catch (err) {
log(`WebRTC connection failed: ${err.message}`, 'system');
updateStatus('disconnected', 'Failed');
disconnectCockpit();
} finally {
connectBtn.disabled = false;
}
}
function disconnectCockpit() {
log('Disconnecting WebRTC Cockpit...');
if (sendControlsInterval) {
clearInterval(sendControlsInterval);
sendControlsInterval = null;
}
if (dataChannel) {
dataChannel.close();
dataChannel = null;
}
if (videoChannel) {
videoChannel.close();
videoChannel = null;
}
if (pc) {
pc.close();
pc = null;
}
videoFeed.style.display = 'none';
videoPlaceholder.style.display = 'flex';
updateStatus('disconnected', 'Disconnected');
connectBtn.innerText = 'Connect';
connectBtn.classList.remove('disconnect');
// Reset Telemetry
telSpeed.innerHTML = '0.00 <span style="font-size: 0.8rem; font-weight: normal;">m/s</span>';
telPing.innerHTML = '0 <span style="font-size: 0.8rem; font-weight: normal;">ms</span>';
telDevice.innerHTML = '-';
telPos.innerHTML = '0.0, 0.0';
}
function setupDataChannel(dc) {
dc.onopen = () => {
log(`Bidirectional DataChannel ('${dc.label}') is OPEN`, 'system');
updateStatus('connected', 'Connected');
connectBtn.innerText = 'Disconnect';
connectBtn.classList.add('disconnect');
// Start sending steering controls at 30Hz
sendControlsInterval = setInterval(sendControls, 33);
};
dc.onclose = () => {
log(`DataChannel ('${dc.label}') is CLOSED`, 'system');
disconnectCockpit();
};
dc.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'telemetry') {
// Update Telemetry Panel
telSpeed.innerHTML = `${msg.speed.toFixed(2)} <span style="font-size: 0.8rem; font-weight: normal;">m/s</span>`;
telDevice.innerHTML = msg.car_id || '-';
telPos.innerHTML = `${msg.x.toFixed(1)}, ${msg.y.toFixed(1)}`;
// Measure RTT
const currentMs = Date.now();
const latency = currentMs - msg.timestamp;
telPing.innerHTML = `${latency} <span style="font-size: 0.8rem; font-weight: normal;">ms</span>`;
if (pingsReceived % 5 === 0) {
log(`Telemetry RX: Speed=${msg.speed.toFixed(2)}m/s Car=${msg.car_id || '-'}`, 'in');
}
pingsReceived++;
} else if (msg.type === 'ack') {
// Log ACK periodically (every 30 frames) to verify loopback
if (pingsReceived % 30 === 0) {
log(`ACK RX from server: applied=${msg.applied}`, 'in');
}
}
} catch (err) {
log(`DataChannel Error: ${err.message}`, 'system');
}
};
}
function setupVideoChannel(dc) {
dc.binaryType = 'arraybuffer';
dc.onopen = () => {
log(`Video DataChannel ('${dc.label}') is OPEN`, 'system');
videoPlaceholder.style.display = 'none';
videoFeed.style.display = 'block';
};
dc.onclose = () => {
log(`Video DataChannel ('${dc.label}') is CLOSED`, 'system');
};
dc.onmessage = (event) => {
// Binary JPEG frames received
const blob = new Blob([event.data], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
videoFeed.onload = () => {
URL.revokeObjectURL(url);
};
videoFeed.src = url;
};
}
function sendControls() {
if (dataChannel && dataChannel.readyState === 'open') {
const payload = {
throttle: currentThrottle,
steering: currentSteer,
brake: currentBrake,
gear: currentGear,
buttons: buttons,
timestamp: Date.now()
};
dataChannel.send(JSON.stringify(payload));
}
}
function updateStatus(state, label) {
statusText.innerText = label;
statusDot.className = 'status-dot';
if (state === 'connected') statusDot.classList.add('connected');
if (state === 'connecting') statusDot.classList.add('connecting');
}
</script>
</body>
</html>