removed device_id from all entities except car. Replaced with car_id . Car -> device_id

This commit is contained in:
2026-07-16 17:40:59 +04:00
parent 41eaf49bb2
commit f6547cd1f1
18 changed files with 126 additions and 78 deletions
+5 -2
View File
@@ -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,
@@ -239,8 +240,9 @@ func carFromWire(w transport.CarWire) catalog.CarMeta {
ColorHex: w.ColorHex,
AvatarURL: w.AvatarURL,
Active: w.Active,
CreatedMs: w.CreatedMs,
UpdatedMs: w.UpdatedMs,
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,
}
}
+15 -18
View File
@@ -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
}
+7 -5
View File
@@ -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)
+16 -9
View File
@@ -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,17 +32,19 @@ 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,
sessions: make(map[string]*driverSession),
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 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 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
+7 -1
View File
@@ -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"
},
+7 -1
View File
@@ -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"
},
+5 -1
View File
@@ -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.
+10
View File
@@ -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 ||
+1
View File
@@ -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"`
+14 -20
View File
@@ -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()
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -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.
+9 -2
View File
@@ -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
}
+2 -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.
_, _ = 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 {
+5 -5
View File
@@ -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;
+9 -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,
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) {
+2
View File
@@ -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": "..." }.