mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
removed device_id from all entities except car. Replaced with car_id . Car -> device_id
This commit is contained in:
@@ -189,6 +189,7 @@ func carToWire(c catalog.CarMeta) transport.CarWire {
|
||||
ColorHex: c.ColorHex,
|
||||
AvatarURL: c.AvatarURL,
|
||||
Active: c.Active,
|
||||
DeviceID: c.DeviceID,
|
||||
// Stats are read-mostly; the server fills them on read.
|
||||
TotalDistanceM: c.TotalDistanceM,
|
||||
TotalRaces: c.TotalRaces,
|
||||
@@ -241,6 +242,7 @@ func carFromWire(w transport.CarWire) catalog.CarMeta {
|
||||
Active: w.Active,
|
||||
CreatedMs: w.CreatedMs,
|
||||
UpdatedMs: w.UpdatedMs,
|
||||
DeviceID: w.DeviceID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +294,7 @@ func carPatchFromWire(p transport.CarPatch) catalog.UpdateCarOptions {
|
||||
Chassis: chassisFromPtr(p.Chassis),
|
||||
Motor: motorFromPtr(p.Motor),
|
||||
Battery: batteryFromPtr(p.Battery),
|
||||
DeviceID: p.DeviceID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ func driversListHandler(svc *drivers.Service) http.HandlerFunc {
|
||||
// @Router /api/drivers/{id} [get]
|
||||
// @Router /api/drivers/{id} [put]
|
||||
// @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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(id, "/device") {
|
||||
driverID := strings.TrimSuffix(id, "/device")
|
||||
if strings.HasSuffix(id, "/car") {
|
||||
driverID := strings.TrimSuffix(id, "/car")
|
||||
if driverID == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
|
||||
return
|
||||
@@ -303,29 +303,26 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
|
||||
return
|
||||
}
|
||||
|
||||
// Read device_id from JSON body or query param
|
||||
var deviceID *int
|
||||
type deviceRequest struct {
|
||||
DeviceID *int `json:"device_id"`
|
||||
// Read car_id from JSON body or query param
|
||||
var carID *string
|
||||
type carRequest struct {
|
||||
CarID *string `json:"car_id"`
|
||||
}
|
||||
var req deviceRequest
|
||||
var req carRequest
|
||||
if r.ContentLength > 0 {
|
||||
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
|
||||
if deviceID == nil {
|
||||
qVal := r.URL.Query().Get("device_id")
|
||||
if carID == nil {
|
||||
qVal := r.URL.Query().Get("car_id")
|
||||
if qVal != "" {
|
||||
if qVal == "null" {
|
||||
deviceID = nil
|
||||
} else if idInt, err := strconv.Atoi(qVal); err == nil {
|
||||
deviceID = &idInt
|
||||
carID = nil
|
||||
} else {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid device_id query parameter")
|
||||
return
|
||||
carID = &qVal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,8 +342,8 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
|
||||
_, _ = 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 {
|
||||
// Select car in lobby (handles constraints and write-through)
|
||||
if err := lobbySvc.SelectCar(driverID, carID); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ func main() {
|
||||
driversSvc := drivers.NewService(drivers.NewPgStore(pool))
|
||||
|
||||
// WebRTC 1-to-1 driver-car proxy service
|
||||
webrtcSvc := NewWebRTCService(logger, engine, lobbySvc, videoSvc)
|
||||
webrtcSvc := NewWebRTCService(logger, engine, lobbySvc, videoSvc, cat)
|
||||
|
||||
// Persist finished races so the /api/races list and keyset pagination
|
||||
// can serve historical data across restarts. The snapshot is best-
|
||||
@@ -577,7 +577,7 @@ func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *c
|
||||
case transport.TypeClientHello:
|
||||
handleClientHello(c, e, cfg, lobbySvc, driversSvc, env)
|
||||
case transport.TypeJoinRace:
|
||||
handleJoinRace(c, e, lobbySvc, env)
|
||||
handleJoinRace(c, e, lobbySvc, cat, env)
|
||||
case transport.TypeLeaveRace:
|
||||
handleLeaveRace(c, e, env)
|
||||
case transport.TypeInputState:
|
||||
@@ -635,7 +635,7 @@ func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config
|
||||
c.Send <- mustEncode(transport.TypeServerHello, hello)
|
||||
}
|
||||
|
||||
func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service, 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)
|
||||
slot := 0
|
||||
name := "anon"
|
||||
@@ -654,8 +654,10 @@ func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Servi
|
||||
driverID = c.ID
|
||||
}
|
||||
var deviceID *int
|
||||
if d, err := lobbySvc.GetDriver(driverID); err == nil {
|
||||
deviceID = d.DeviceID
|
||||
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)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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"
|
||||
@@ -31,16 +32,18 @@ type WebRTCService struct {
|
||||
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) *WebRTCService {
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -93,10 +96,14 @@ func (s *WebRTCService) ConnectHandler() http.HandlerFunc {
|
||||
|
||||
// Resolve device ID for this driver
|
||||
deviceID := uint8(1) // default fallback
|
||||
if driver, err := s.lobbySvc.GetDriver(req.DriverID); err == nil && driver.DeviceID != nil {
|
||||
deviceID = uint8(*driver.DeviceID)
|
||||
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 no physical device_id assigned in lobbySvc. Defaulting to device 1", "driver_id", req.DriverID)
|
||||
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
|
||||
|
||||
+7
-1
@@ -948,7 +948,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/drivers/{id}/device": {
|
||||
"/api/drivers/{id}/car": {
|
||||
"put": {
|
||||
"description": "Dispatches on HTTP method. Path ` + "`" + `/api/drivers/by-nick/{nick}` + "`" + ` is a convenience that returns the driver by 3-letter nickname.",
|
||||
"produces": [
|
||||
@@ -2242,6 +2242,9 @@ const docTemplate = `{
|
||||
"color_hex": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"drive": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2309,6 +2312,9 @@ const docTemplate = `{
|
||||
"created_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"device_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"drive": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -946,7 +946,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/drivers/{id}/device": {
|
||||
"/api/drivers/{id}/car": {
|
||||
"put": {
|
||||
"description": "Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is a convenience that returns the driver by 3-letter nickname.",
|
||||
"produces": [
|
||||
@@ -2240,6 +2240,9 @@
|
||||
"color_hex": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"drive": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2307,6 +2310,9 @@
|
||||
"created_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"device_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"drive": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -39,6 +39,8 @@ definitions:
|
||||
$ref: '#/definitions/transport.ChassisWire'
|
||||
color_hex:
|
||||
type: string
|
||||
device_id:
|
||||
type: integer
|
||||
drive:
|
||||
type: string
|
||||
height_mm:
|
||||
@@ -83,6 +85,8 @@ definitions:
|
||||
type: string
|
||||
created_ms:
|
||||
type: integer
|
||||
device_id:
|
||||
type: integer
|
||||
drive:
|
||||
type: string
|
||||
height_mm:
|
||||
@@ -1484,7 +1488,7 @@ paths:
|
||||
summary: Get / update / delete a driver by id
|
||||
tags:
|
||||
- drivers
|
||||
/api/drivers/{id}/device:
|
||||
/api/drivers/{id}/car:
|
||||
put:
|
||||
description: Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is
|
||||
a convenience that returns the driver by 3-letter nickname.
|
||||
|
||||
@@ -247,6 +247,7 @@ type CreateCarOptions struct {
|
||||
ColorHex string
|
||||
AvatarURL string
|
||||
Active *bool // optional; defaults to true
|
||||
DeviceID *int
|
||||
}
|
||||
|
||||
// Validate sanity-checks dimensions and required fields.
|
||||
@@ -319,6 +320,7 @@ func (s *Service) CreateCar(ctx context.Context, opts CreateCarOptions) (CarMeta
|
||||
ColorHex: opts.ColorHex,
|
||||
AvatarURL: opts.AvatarURL,
|
||||
Active: active,
|
||||
DeviceID: opts.DeviceID,
|
||||
CreatedMs: now,
|
||||
UpdatedMs: now,
|
||||
}
|
||||
@@ -345,6 +347,7 @@ type UpdateCarOptions struct {
|
||||
Motor *MotorSpec
|
||||
Battery *BatterySpec
|
||||
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.
|
||||
@@ -403,6 +406,13 @@ func (s *Service) UpdateCar(ctx context.Context, id string, opts UpdateCarOption
|
||||
if opts.Drive != nil {
|
||||
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()
|
||||
|
||||
if c.LengthMm < minCarLengthMm || c.LengthMm > maxCarLengthMm ||
|
||||
|
||||
@@ -146,6 +146,7 @@ type CarMeta struct {
|
||||
Motor MotorSpec `json:"motor"`
|
||||
Battery BatterySpec `json:"battery"`
|
||||
Drive Drivetrain `json:"drive"`
|
||||
DeviceID *int `json:"device_id,omitempty"` // mapped physical ESP32 device ID
|
||||
|
||||
// Performance envelope.
|
||||
TopSpeedMs float64 `json:"top_speed_ms"`
|
||||
|
||||
@@ -187,7 +187,7 @@ func (s *Service) RemoveDriver(id string) {
|
||||
d.Status = DriverStatusOffline
|
||||
d.LastSeenMs = time.Now().UnixMilli()
|
||||
d.RaceID = ""
|
||||
d.DeviceID = nil
|
||||
d.CarID = nil
|
||||
hook := s.onRaceRemoved
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -244,9 +244,9 @@ func (s *Service) AddDriverToRace(driverID, raceID string) error {
|
||||
s.mu.Unlock()
|
||||
return ErrDriverNotFound
|
||||
}
|
||||
if d.DeviceID == nil {
|
||||
if d.CarID == nil || *d.CarID == "" {
|
||||
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]
|
||||
if !ok {
|
||||
@@ -306,7 +306,7 @@ func (s *Service) RemoveDriverFromRace(driverID string) {
|
||||
}
|
||||
d.Status = DriverStatusIdle
|
||||
d.RaceID = ""
|
||||
d.DeviceID = nil
|
||||
d.CarID = nil
|
||||
prevRaceID := d.RaceID
|
||||
hook := s.onRaceRemoved
|
||||
s.mu.Unlock()
|
||||
@@ -349,7 +349,7 @@ func (s *Service) SetRaceStatus(raceID string, status RaceStatus) error {
|
||||
if d, ok := s.drivers[did]; ok {
|
||||
d.Status = DriverStatusIdle
|
||||
d.RaceID = ""
|
||||
d.DeviceID = nil
|
||||
d.CarID = nil
|
||||
affected = append(affected, *d)
|
||||
}
|
||||
}
|
||||
@@ -454,16 +454,10 @@ func (s *Service) GetDriver(driverID string) (DriverMeta, error) {
|
||||
return *d, nil
|
||||
}
|
||||
|
||||
// SelectDevice assigns a physical device ID (0..127) to a driver.
|
||||
// Pass deviceID = nil to release the device.
|
||||
// Returns an error if the device is already taken by another online driver.
|
||||
func (s *Service) SelectDevice(driverID string, deviceID *int) error {
|
||||
if deviceID != nil {
|
||||
if *deviceID < 0 || *deviceID > 127 {
|
||||
return fmt.Errorf("invalid device ID: must be 0..127")
|
||||
}
|
||||
}
|
||||
|
||||
// SelectCar assigns a catalog car ID to a driver.
|
||||
// If carID is nil, it clears the driver's car assignment.
|
||||
// It returns an error if the car is already taken by another active driver.
|
||||
func (s *Service) SelectCar(driverID string, carID *string) error {
|
||||
s.mu.Lock()
|
||||
d, ok := s.drivers[driverID]
|
||||
if !ok {
|
||||
@@ -471,17 +465,17 @@ func (s *Service) SelectDevice(driverID string, deviceID *int) error {
|
||||
return ErrDriverNotFound
|
||||
}
|
||||
|
||||
// Enforce that device cannot be selected if already assigned to another driver.
|
||||
if deviceID != nil {
|
||||
// Enforce that car cannot be selected if already assigned to another driver.
|
||||
if carID != nil && *carID != "" {
|
||||
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()
|
||||
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
|
||||
s.mu.Unlock()
|
||||
|
||||
|
||||
@@ -75,9 +75,9 @@ func TestQuickPlayJoinsFirstRace(t *testing.T) {
|
||||
s := NewService()
|
||||
_, _ = s.AddDriver("d1", "Alice", "")
|
||||
_, _ = s.AddDriver("d2", "Bob", "")
|
||||
dev1, dev2 := 1, 2
|
||||
_ = s.SelectDevice("d1", &dev1)
|
||||
_ = s.SelectDevice("d2", &dev2)
|
||||
car1, car2 := "car-1", "car-2"
|
||||
_ = s.SelectCar("d1", &car1)
|
||||
_ = s.SelectCar("d2", &car2)
|
||||
r1, _ := s.QuickPlay("d1", 4)
|
||||
r2, err := s.QuickPlay("d2", 4)
|
||||
if err != nil {
|
||||
|
||||
@@ -65,7 +65,7 @@ type DriverMeta struct {
|
||||
LastSeenMs int64 `json:"last_seen_ms"`
|
||||
Country string `json:"country,omitempty"` // optional, for UI
|
||||
AvatarHue int `json:"avatar_hue,omitempty"` // 0..360, deterministic per id
|
||||
DeviceID *int `json:"-"`
|
||||
CarID *string `json:"car_id,omitempty"`
|
||||
}
|
||||
|
||||
// Snapshot is the immutable view broadcast to clients.
|
||||
|
||||
@@ -547,8 +547,15 @@ 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)
|
||||
cars := []string{
|
||||
"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 {
|
||||
// ignore: race may be at capacity
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ func (s *Service) RestoreFromDB(ctx context.Context) (int, int, error) {
|
||||
// AddDriver+SetDriverProfile and accept a no-op mirror.
|
||||
_, _ = s.lobby.AddDriver(d.ID, d.Name, "")
|
||||
s.lobby.SetDriverProfile(d.ID, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag)
|
||||
if d.DeviceID != nil {
|
||||
_ = s.lobby.SelectDevice(d.ID, d.DeviceID)
|
||||
if d.CarID != nil {
|
||||
_ = s.lobby.SelectCar(d.ID, d.CarID)
|
||||
}
|
||||
}
|
||||
for _, r := range races {
|
||||
|
||||
@@ -391,7 +391,7 @@ func (s *PgStore) UpsertLobbyDriver(ctx context.Context, d lobby.DriverMeta) err
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO lobby_drivers
|
||||
(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)
|
||||
ON CONFLICT (driver_id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
@@ -402,9 +402,9 @@ func (s *PgStore) UpsertLobbyDriver(ctx context.Context, d lobby.DriverMeta) err
|
||||
status = EXCLUDED.status,
|
||||
current_race_id = EXCLUDED.current_race_id,
|
||||
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,
|
||||
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 {
|
||||
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) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
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`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -432,7 +432,7 @@ func (s *PgStore) ListLobbyDrivers(ctx context.Context) ([]lobby.DriverMeta, err
|
||||
var d lobby.DriverMeta
|
||||
var status string
|
||||
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
|
||||
}
|
||||
d.Status = lobby.DriverStatus(status)
|
||||
|
||||
@@ -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;
|
||||
@@ -142,7 +142,7 @@ func (s *PgStore) loadCars(ctx context.Context) ([]catalog.CarMeta, error) {
|
||||
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
|
||||
drive, top_speed_ms, color_hex, avatar_url, active,
|
||||
total_distance_m, total_races, total_laps, best_lap_ms,
|
||||
created_ms, updated_ms
|
||||
created_ms, updated_ms, device_id
|
||||
FROM cars
|
||||
ORDER BY id`)
|
||||
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.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active,
|
||||
&c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs,
|
||||
&c.CreatedMs, &c.UpdatedMs,
|
||||
&c.CreatedMs, &c.UpdatedMs, &c.DeviceID,
|
||||
); err != nil {
|
||||
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,
|
||||
drive, top_speed_ms, color_hex, avatar_url, active,
|
||||
total_distance_m, total_races, total_laps, best_lap_ms,
|
||||
created_ms, updated_ms)
|
||||
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)
|
||||
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,$33)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
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_laps = EXCLUDED.total_laps,
|
||||
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.LengthMm, c.WidthMm, c.HeightMm, c.WeightG,
|
||||
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,
|
||||
string(c.Drive), c.TopSpeedMs, c.ColorHex, c.AvatarURL, c.Active,
|
||||
c.TotalDistanceM, c.TotalRaces, c.TotalLaps, c.BestLapMs,
|
||||
c.CreatedMs, c.UpdatedMs,
|
||||
c.CreatedMs, c.UpdatedMs, c.DeviceID,
|
||||
)
|
||||
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,
|
||||
drive, top_speed_ms, color_hex, avatar_url, active,
|
||||
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,
|
||||
).Scan(
|
||||
&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.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active,
|
||||
&c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs,
|
||||
&c.CreatedMs, &c.UpdatedMs,
|
||||
&c.CreatedMs, &c.UpdatedMs, &c.DeviceID,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
||||
@@ -478,6 +478,7 @@ type CarWire struct {
|
||||
|
||||
CreatedMs int64 `json:"created_ms"`
|
||||
UpdatedMs int64 `json:"updated_ms"`
|
||||
DeviceID *int `json:"device_id,omitempty"`
|
||||
}
|
||||
|
||||
// CarListRequest: payload { "scope": "all|system|public|private", "owner_id": "..." }.
|
||||
@@ -519,6 +520,7 @@ type CarPatch struct {
|
||||
ColorHex *string `json:"color_hex,omitempty"`
|
||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
DeviceID *int `json:"device_id,omitempty"`
|
||||
}
|
||||
|
||||
// CarDeleteRequest: payload { "id": "..." }.
|
||||
|
||||
Reference in New Issue
Block a user