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,
|
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,
|
||||||
@@ -239,8 +240,9 @@ func carFromWire(w transport.CarWire) catalog.CarMeta {
|
|||||||
ColorHex: w.ColorHex,
|
ColorHex: w.ColorHex,
|
||||||
AvatarURL: w.AvatarURL,
|
AvatarURL: w.AvatarURL,
|
||||||
Active: w.Active,
|
Active: w.Active,
|
||||||
CreatedMs: w.CreatedMs,
|
CreatedMs: w.CreatedMs,
|
||||||
UpdatedMs: w.UpdatedMs,
|
UpdatedMs: w.UpdatedMs,
|
||||||
|
DeviceID: w.DeviceID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ func main() {
|
|||||||
driversSvc := drivers.NewService(drivers.NewPgStore(pool))
|
driversSvc := drivers.NewService(drivers.NewPgStore(pool))
|
||||||
|
|
||||||
// WebRTC 1-to-1 driver-car proxy service
|
// 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
|
// 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-
|
||||||
@@ -577,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, lobbySvc, 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:
|
||||||
@@ -635,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, 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)
|
payload, _ := env.Payload.(map[string]any)
|
||||||
slot := 0
|
slot := 0
|
||||||
name := "anon"
|
name := "anon"
|
||||||
@@ -654,8 +654,10 @@ func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Servi
|
|||||||
driverID = c.ID
|
driverID = c.ID
|
||||||
}
|
}
|
||||||
var deviceID *int
|
var deviceID *int
|
||||||
if d, err := lobbySvc.GetDriver(driverID); err == nil {
|
if d, err := lobbySvc.GetDriver(driverID); err == nil && d.CarID != nil && *d.CarID != "" {
|
||||||
deviceID = d.DeviceID
|
if carMeta, ok := cat.GetCar(*d.CarID); ok {
|
||||||
|
deviceID = carMeta.DeviceID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
car, err := e.AddCar(c.ID, name, slot, deviceID)
|
car, err := e.AddCar(c.ID, name, slot, deviceID)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
"github.com/x0gp/server/internal/catalog"
|
||||||
"github.com/x0gp/server/internal/control"
|
"github.com/x0gp/server/internal/control"
|
||||||
"github.com/x0gp/server/internal/lobby"
|
"github.com/x0gp/server/internal/lobby"
|
||||||
"github.com/x0gp/server/internal/transport"
|
"github.com/x0gp/server/internal/transport"
|
||||||
@@ -31,17 +32,19 @@ type WebRTCService struct {
|
|||||||
engine *control.Engine
|
engine *control.Engine
|
||||||
lobbySvc *lobby.Service
|
lobbySvc *lobby.Service
|
||||||
videoSvc *UDPVideoService
|
videoSvc *UDPVideoService
|
||||||
|
catalogSvc *catalog.Service
|
||||||
sessions map[string]*driverSession
|
sessions map[string]*driverSession
|
||||||
sessionsMu sync.RWMutex
|
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{
|
return &WebRTCService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
engine: e,
|
engine: e,
|
||||||
lobbySvc: lobbySvc,
|
lobbySvc: lobbySvc,
|
||||||
videoSvc: videoSvc,
|
videoSvc: videoSvc,
|
||||||
sessions: make(map[string]*driverSession),
|
catalogSvc: catalogSvc,
|
||||||
|
sessions: make(map[string]*driverSession),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,10 +96,14 @@ func (s *WebRTCService) ConnectHandler() http.HandlerFunc {
|
|||||||
|
|
||||||
// Resolve device ID for this driver
|
// Resolve device ID for this driver
|
||||||
deviceID := uint8(1) // default fallback
|
deviceID := uint8(1) // default fallback
|
||||||
if driver, err := s.lobbySvc.GetDriver(req.DriverID); err == nil && driver.DeviceID != nil {
|
if driver, err := s.lobbySvc.GetDriver(req.DriverID); err == nil && driver.CarID != nil && *driver.CarID != "" {
|
||||||
deviceID = uint8(*driver.DeviceID)
|
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 {
|
} 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 no car_id assigned in lobbySvc. Defaulting to device 1", "driver_id", req.DriverID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create PeerConnection
|
// Create PeerConnection
|
||||||
|
|||||||
+7
-1
@@ -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": [
|
||||||
@@ -2242,6 +2242,9 @@ const docTemplate = `{
|
|||||||
"color_hex": {
|
"color_hex": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"device_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"drive": {
|
"drive": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -2309,6 +2312,9 @@ const docTemplate = `{
|
|||||||
"created_ms": {
|
"created_ms": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"device_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"drive": {
|
"drive": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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": [
|
||||||
@@ -2240,6 +2240,9 @@
|
|||||||
"color_hex": {
|
"color_hex": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"device_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"drive": {
|
"drive": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -2307,6 +2310,9 @@
|
|||||||
"created_ms": {
|
"created_ms": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"device_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"drive": {
|
"drive": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -1484,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.
|
||||||
|
|||||||
@@ -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 ||
|
||||||
|
|||||||
@@ -146,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"`
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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:"-"`
|
CarID *string `json:"car_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot is the immutable view broadcast to clients.
|
// 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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
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) {
|
||||||
|
|||||||
@@ -478,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": "..." }.
|
||||||
@@ -519,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": "..." }.
|
||||||
|
|||||||
Reference in New Issue
Block a user