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
@@ -26,6 +26,7 @@ import (
"github.com/x0gp/server/internal/clans"
"github.com/x0gp/server/internal/drivers"
"github.com/x0gp/server/internal/lobby"
"github.com/x0gp/server/internal/transport"
)
@@ -282,13 +283,84 @@ func driversListHandler(svc *drivers.Service) http.HandlerFunc {
// @Router /api/drivers/{id} [get]
// @Router /api/drivers/{id} [put]
// @Router /api/drivers/{id} [delete]
func driversByIDHandler(svc *drivers.Service) http.HandlerFunc {
// @Router /api/drivers/{id}/device [put]
func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/drivers/")
if id == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return
}
if strings.HasSuffix(id, "/device") {
driverID := strings.TrimSuffix(id, "/device")
if driverID == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return
}
if r.Method != http.MethodPut {
w.Header().Set("Allow", "PUT")
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method)
return
}
// Read device_id from JSON body or query param
var deviceID *int
type deviceRequest struct {
DeviceID *int `json:"device_id"`
}
var req deviceRequest
if r.ContentLength > 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
deviceID = req.DeviceID
}
}
// Fallback to query parameter if body didn't provide it
if deviceID == nil {
qVal := r.URL.Query().Get("device_id")
if qVal != "" {
if qVal == "null" {
deviceID = nil
} else if idInt, err := strconv.Atoi(qVal); err == nil {
deviceID = &idInt
} else {
writeError(w, http.StatusBadRequest, "bad_request", "invalid device_id query parameter")
return
}
}
}
// Validate driver exists in DB
drv, err := svc.Get(r.Context(), driverID)
if err != nil {
if errors.Is(err, drivers.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", "driver not found")
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
// Make sure driver is registered in lobby
_, _ = lobbySvc.AddDriver(driverID, drv.Name, "")
lobbySvc.SetDriverProfile(driverID, drv.Nickname, drv.AvatarURL, drv.ClanID, "")
// Select device in lobby (handles constraints and write-through)
if err := lobbySvc.SelectDevice(driverID, deviceID); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
// Return updated lobby driver status
lobbyDriver, err := lobbySvc.GetDriver(driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
writeJSON(w, http.StatusOK, lobbyDriver)
return
}
if strings.HasPrefix(id, "by-nick/") {
nick := strings.TrimPrefix(id, "by-nick/")
if nick == "" {