feat(server): integrate UDP video service and normalize database schema

- Implement UDPVideoService to stream MJPEG video frames from ESP32s via UDP and send control commands.
- Normalize race database schema:
  - Remove redundant driver_ids array from races table (migration 011).
  - Move per-driver outcomes (total_time_ms, best_lap_ms, position) to race_drivers table (migration 012).
  - Add device_id column to lobby_drivers to link active physical devices (migration 013).
- Update WebSocket handler to pass lobbySvc, driversSvc, and videoSvc for driver identity tracking, syncing in-memory driver metadata, and forwarding WS controls to physical cars.
- Add CORS middleware to HTTP routes.
- Regenerate Swagger documentation.
This commit is contained in:
x0gp
2026-07-06 12:38:34 +04:00
parent 978d36c505
commit 49351b4928
19 changed files with 1406 additions and 132 deletions
+30 -28
View File
@@ -394,9 +394,9 @@ func (r *Runner) makeFinished(idx int) races.FinishedRace {
}
bestLapMs := int64(20_000 + rng.Intn(8_000))
// Podium: top-3 by total time. The winner always finishes; the
// remaining drivers are sorted by a per-driver total time derived
// from the winner's best lap.
// Per-driver total time. The winner is fastest; the rest are
// winner's time + 2..16 seconds. Build the Results slice with
// computed positions.
perDriver := make([]int64, len(picked))
for i, d := range picked {
if d.ID == winner.ID {
@@ -419,35 +419,35 @@ func (r *Runner) makeFinished(idx int) races.FinishedRace {
all[j-1], all[j] = all[j], all[j-1]
}
}
podium := make([]races.PodiumEntry, 0, 3)
for i := 0; i < len(all) && i < 3; i++ {
podium = append(podium, races.PodiumEntry{
Position: i + 1,
DriverID: all[i].d.ID,
Name: all[i].d.Nickname,
TotalTimeMs: all[i].time,
results := make([]races.DriverResult, 0, len(all))
for i, row := range all {
pos := i + 1
// Podium only for the top-3; the rest are just DNF-ish finishers
// (they have a time and a position).
results = append(results, races.DriverResult{
DriverID: row.d.ID,
TotalTimeMs: row.time,
BestLapMs: bestLapMs,
Position: &pos,
})
}
return races.FinishedRace{
ID: fmt.Sprintf("seed-finished-%03d", idx+1),
Name: fmt.Sprintf("Seed Race #%d (%s)", idx+1, track),
TrackID: track,
MaxCars: 4,
Laps: laps,
TimeLimitS: 0,
DriverIDs: driverIDs,
Status: "finished",
CreatedMs: finishedMs - randDuration(rng, 5, 120).Milliseconds(),
StartedMs: startedMs,
FinishedMs: finishedMs,
DurationMs: finishedMs - startedMs,
TotalLaps: totalLaps,
TotalDrivers: len(driverIDs),
WinnerDriverID: winner.ID,
WinnerName: winner.Nickname,
BestLapMs: bestLapMs,
Podium: podium,
ID: fmt.Sprintf("seed-finished-%03d", idx+1),
Name: fmt.Sprintf("Seed Race #%d (%s)", idx+1, track),
TrackID: track,
MaxCars: 4,
Laps: laps,
TimeLimitS: 0,
DriverIDs: driverIDs,
Status: "finished",
CreatedMs: finishedMs - randDuration(rng, 5, 120).Milliseconds(),
StartedMs: startedMs,
FinishedMs: finishedMs,
DurationMs: finishedMs - startedMs,
TotalLaps: totalLaps,
TotalDrivers: len(driverIDs),
Results: results,
}
}
@@ -481,6 +481,8 @@ func (r *Runner) createLive(status lobby.RaceStatus, idx int) (lobby.RaceMeta, e
continue
}
r.lb.SetDriverProfile(d.ID, d.Nickname, "", d.ClanID, d.ClanTag)
devID := i
_ = r.lb.SelectDevice(d.ID, &devID)
if err := r.lb.AddDriverToRace(d.ID, meta.ID); err != nil {
// ignore: race may be at capacity
}