mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 12:07:23 -04:00
Subscribe to TeamRadio on the F1 live feed, parse defensive capture snapshots and patches, and relay capped captures through live snapshots with the SessionInfo static path. Add the frontend CDN URL helper and ticker using one shared audio element.
879 lines
22 KiB
Go
879 lines
22 KiB
Go
package live
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"compress/zlib"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// State accumulates live timing updates from SignalR topic payloads.
|
|
type State struct {
|
|
Drivers map[string]LiveDriverData
|
|
DriverInfo map[string]F1DriverListEntry
|
|
Tyres map[string]LiveTyreData
|
|
Telemetry map[string]LiveTelemetryData
|
|
Positions map[string]LivePositionData
|
|
Stints map[string][]LiveStintData
|
|
RCMessages []LiveRCMessage
|
|
Weather LiveWeatherData
|
|
Session LiveSessionMeta
|
|
TeamRadio []LiveRadioCapture
|
|
TrackStatus string
|
|
CurrentLap int
|
|
TotalLaps int
|
|
Clock string
|
|
ClockRefTime time.Time
|
|
ClockExtrapolating bool
|
|
positionUpdated bool
|
|
snapshotUpdated bool
|
|
}
|
|
|
|
const signalRRecordSeparator = byte(0x1e)
|
|
const maxTeamRadioCaptures = 20
|
|
|
|
// NewState returns an empty live timing accumulator.
|
|
func NewState() *State {
|
|
return &State{
|
|
Drivers: make(map[string]LiveDriverData),
|
|
DriverInfo: make(map[string]F1DriverListEntry),
|
|
Tyres: make(map[string]LiveTyreData),
|
|
Telemetry: make(map[string]LiveTelemetryData),
|
|
Positions: make(map[string]LivePositionData),
|
|
Stints: make(map[string][]LiveStintData),
|
|
}
|
|
}
|
|
|
|
// Snapshot returns a defensive copy of the current state.
|
|
func (s *State) Snapshot() LiveStreamData {
|
|
cpyDrivers := make(map[string]LiveDriverData, len(s.Drivers))
|
|
for k, v := range s.Drivers {
|
|
cpyDrivers[k] = v
|
|
}
|
|
cpyInfo := make(map[string]F1DriverListEntry, len(s.DriverInfo))
|
|
for k, v := range s.DriverInfo {
|
|
cpyInfo[k] = v
|
|
}
|
|
cpyTyres := make(map[string]LiveTyreData, len(s.Tyres))
|
|
for k, v := range s.Tyres {
|
|
cpyTyres[k] = v
|
|
}
|
|
cpyTelemetry := make(map[string]LiveTelemetryData, len(s.Telemetry))
|
|
for k, v := range s.Telemetry {
|
|
cpyTelemetry[k] = v
|
|
}
|
|
cpyPositions := make(map[string]LivePositionData, len(s.Positions))
|
|
for k, v := range s.Positions {
|
|
cpyPositions[k] = v
|
|
}
|
|
cpyRC := make([]LiveRCMessage, len(s.RCMessages))
|
|
copy(cpyRC, s.RCMessages)
|
|
cpyRadio := make([]LiveRadioCapture, len(s.TeamRadio))
|
|
copy(cpyRadio, s.TeamRadio)
|
|
cpyStints := make(map[string][]LiveStintData, len(s.Stints))
|
|
for k, v := range s.Stints {
|
|
st := make([]LiveStintData, len(v))
|
|
copy(st, v)
|
|
cpyStints[k] = st
|
|
}
|
|
|
|
return LiveStreamData{
|
|
Drivers: cpyDrivers,
|
|
DriverInfo: cpyInfo,
|
|
Tyres: cpyTyres,
|
|
Telemetry: cpyTelemetry,
|
|
RCMessages: cpyRC,
|
|
Weather: s.Weather,
|
|
Session: s.Session,
|
|
TeamRadio: cpyRadio,
|
|
TrackStatus: s.TrackStatus,
|
|
CurrentLap: s.CurrentLap,
|
|
TotalLaps: s.TotalLaps,
|
|
Clock: s.Clock,
|
|
ClockRefTime: s.ClockRefTime,
|
|
ClockExtrapolating: s.ClockExtrapolating,
|
|
Stints: cpyStints,
|
|
Positions: cpyPositions,
|
|
PositionUpdated: s.positionUpdated,
|
|
SnapshotUpdated: s.snapshotUpdated,
|
|
}
|
|
}
|
|
|
|
// ProcessMessage parses a raw SignalR WebSocket frame and applies any updates.
|
|
func (s *State) ProcessMessage(message []byte) bool {
|
|
s.clearTransientFlags()
|
|
|
|
var parsed F1SignalRMessage
|
|
if err := json.Unmarshal(message, &parsed); err != nil {
|
|
return false
|
|
}
|
|
|
|
updated := false
|
|
|
|
if len(parsed.R) > 2 {
|
|
var rMap map[string]json.RawMessage
|
|
if err := json.Unmarshal(parsed.R, &rMap); err == nil {
|
|
for topic, data := range rMap {
|
|
if s.ProcessTopic(topic, data) {
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, m := range parsed.M {
|
|
if len(m.A) > 1 {
|
|
var topic string
|
|
json.Unmarshal(m.A[0], &topic)
|
|
if s.ProcessTopic(topic, m.A[1]) {
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return updated
|
|
}
|
|
|
|
// ProcessCoreMessage parses one or more SignalR Core JSON frames and applies
|
|
// completion snapshots and feed deltas from the current official F1 live timing hub.
|
|
func (s *State) ProcessCoreMessage(message []byte) bool {
|
|
s.clearTransientFlags()
|
|
|
|
updated := false
|
|
for _, frame := range splitSignalRFrames(message) {
|
|
var envelope struct {
|
|
Type int `json:"type"`
|
|
Target string `json:"target"`
|
|
Args []json.RawMessage `json:"arguments"`
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
if err := json.Unmarshal(frame, &envelope); err != nil {
|
|
continue
|
|
}
|
|
|
|
switch envelope.Type {
|
|
case 1:
|
|
if envelope.Target != "feed" || len(envelope.Args) < 2 {
|
|
continue
|
|
}
|
|
var topic string
|
|
if err := json.Unmarshal(envelope.Args[0], &topic); err != nil {
|
|
continue
|
|
}
|
|
if s.ProcessTopic(topic, envelope.Args[1]) {
|
|
updated = true
|
|
}
|
|
case 3:
|
|
if len(envelope.Result) == 0 || string(envelope.Result) == "null" {
|
|
continue
|
|
}
|
|
var result map[string]json.RawMessage
|
|
if err := json.Unmarshal(envelope.Result, &result); err != nil {
|
|
continue
|
|
}
|
|
for topic, data := range result {
|
|
if s.ProcessTopic(topic, data) {
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
|
|
// ProcessTopic applies a single topic payload to the accumulator.
|
|
func (s *State) ProcessTopic(topic string, data json.RawMessage) bool {
|
|
updated := false
|
|
baseTopic := strings.TrimSuffix(topic, ".z")
|
|
if topic != baseTopic {
|
|
var ok bool
|
|
data, ok = inflateTopicPayload(data)
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
switch baseTopic {
|
|
case "TimingData":
|
|
var td struct {
|
|
Lines map[string]json.RawMessage `json:"Lines"`
|
|
}
|
|
if json.Unmarshal(data, &td) == nil {
|
|
for num, lineRaw := range td.Lines {
|
|
var line F1TimingLine
|
|
if json.Unmarshal(lineRaw, &line) == nil {
|
|
updateDriver(s.Drivers, num, line)
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
case "Position":
|
|
updated = s.updatePositions(data)
|
|
case "CarData":
|
|
updated = s.updateTelemetry(data)
|
|
case "DriverList":
|
|
var dlMap map[string]json.RawMessage
|
|
if json.Unmarshal(data, &dlMap) == nil {
|
|
for num, entryRaw := range dlMap {
|
|
var entry F1DriverListEntry
|
|
if json.Unmarshal(entryRaw, &entry) == nil && entry.Tla != "" {
|
|
s.DriverInfo[num] = entry
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
case "LapCount":
|
|
var lc struct {
|
|
CurrentLap json.Number `json:"CurrentLap"`
|
|
TotalLaps json.Number `json:"TotalLaps"`
|
|
}
|
|
if json.Unmarshal(data, &lc) == nil {
|
|
if v, err := lc.CurrentLap.Int64(); err == nil {
|
|
s.CurrentLap = int(v)
|
|
}
|
|
if v, err := lc.TotalLaps.Int64(); err == nil {
|
|
s.TotalLaps = int(v)
|
|
}
|
|
updated = true
|
|
}
|
|
case "ExtrapolatedClock":
|
|
var ec struct {
|
|
Remaining string `json:"Remaining"`
|
|
Utc string `json:"Utc"`
|
|
Extrapolating bool `json:"Extrapolating"`
|
|
}
|
|
if json.Unmarshal(data, &ec) == nil && ec.Remaining != "" {
|
|
s.Clock = ec.Remaining
|
|
s.ClockExtrapolating = ec.Extrapolating
|
|
if ec.Utc != "" {
|
|
if t, err := time.Parse(time.RFC3339, ec.Utc); err == nil {
|
|
s.ClockRefTime = t
|
|
} else if t, err := time.Parse("2006-01-02T15:04:05.999Z", ec.Utc); err == nil {
|
|
s.ClockRefTime = t
|
|
} else {
|
|
s.ClockRefTime = time.Now()
|
|
}
|
|
} else {
|
|
s.ClockRefTime = time.Now()
|
|
}
|
|
updated = true
|
|
}
|
|
case "TrackStatus":
|
|
var ts struct {
|
|
Status string `json:"Status"`
|
|
Message string `json:"Message"`
|
|
}
|
|
if json.Unmarshal(data, &ts) == nil && ts.Status != "" {
|
|
s.TrackStatus = ts.Status
|
|
updated = true
|
|
}
|
|
case "RaceControlMessages":
|
|
var rcm struct {
|
|
Messages json.RawMessage `json:"Messages"`
|
|
}
|
|
if json.Unmarshal(data, &rcm) == nil {
|
|
for _, msgRaw := range indexedRawValues(rcm.Messages) {
|
|
var msg struct {
|
|
Utc string `json:"Utc"`
|
|
Category string `json:"Category"`
|
|
Flag string `json:"Flag"`
|
|
Message string `json:"Message"`
|
|
Lap int `json:"Lap"`
|
|
}
|
|
if json.Unmarshal(msgRaw.Raw, &msg) == nil && msg.Message != "" {
|
|
t := ""
|
|
if len(msg.Utc) >= 19 {
|
|
t = msg.Utc[11:16]
|
|
}
|
|
s.RCMessages = append(s.RCMessages, LiveRCMessage{
|
|
Time: t,
|
|
Category: msg.Category,
|
|
Flag: msg.Flag,
|
|
Message: msg.Message,
|
|
Lap: msg.Lap,
|
|
})
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
case "WeatherData":
|
|
var wd struct {
|
|
AirTemp json.Number `json:"AirTemp"`
|
|
TrackTemp json.Number `json:"TrackTemp"`
|
|
Humidity json.Number `json:"Humidity"`
|
|
WindSpeed json.Number `json:"WindSpeed"`
|
|
WindDirection json.Number `json:"WindDirection"`
|
|
Rainfall json.Number `json:"Rainfall"`
|
|
}
|
|
if json.Unmarshal(data, &wd) == nil {
|
|
if v, err := wd.AirTemp.Float64(); err == nil {
|
|
s.Weather.AirTemp = v
|
|
}
|
|
if v, err := wd.TrackTemp.Float64(); err == nil {
|
|
s.Weather.TrackTemp = v
|
|
}
|
|
if v, err := wd.Humidity.Float64(); err == nil {
|
|
s.Weather.Humidity = v
|
|
}
|
|
if v, err := wd.WindSpeed.Float64(); err == nil {
|
|
s.Weather.WindSpeed = v
|
|
}
|
|
if v, err := wd.WindDirection.Int64(); err == nil {
|
|
s.Weather.WindDir = int(v)
|
|
}
|
|
if v, err := wd.Rainfall.Float64(); err == nil {
|
|
s.Weather.Rainfall = v > 0
|
|
}
|
|
updated = true
|
|
}
|
|
case "SessionInfo":
|
|
var si struct {
|
|
Meeting struct {
|
|
Name string `json:"Name"`
|
|
Circuit struct {
|
|
ShortName string `json:"ShortName"`
|
|
} `json:"Circuit"`
|
|
} `json:"Meeting"`
|
|
Name string `json:"Name"`
|
|
Type string `json:"Type"`
|
|
Path string `json:"Path"`
|
|
}
|
|
if json.Unmarshal(data, &si) == nil {
|
|
if si.Meeting.Name != "" {
|
|
s.Session.MeetingName = si.Meeting.Name
|
|
}
|
|
if si.Meeting.Circuit.ShortName != "" {
|
|
s.Session.CircuitName = si.Meeting.Circuit.ShortName
|
|
}
|
|
if si.Name != "" {
|
|
s.Session.SessionName = si.Name
|
|
}
|
|
if si.Type != "" {
|
|
s.Session.SessionType = si.Type
|
|
}
|
|
if si.Path != "" {
|
|
s.Session.Path = si.Path
|
|
}
|
|
updated = true
|
|
}
|
|
case "TeamRadio":
|
|
updated = s.updateTeamRadio(data)
|
|
case "CurrentTyres":
|
|
var ct map[string]json.RawMessage
|
|
if json.Unmarshal(data, &ct) == nil {
|
|
for num, raw := range ct {
|
|
if num == "_kf" {
|
|
continue
|
|
}
|
|
var td struct {
|
|
Compound string `json:"Compound"`
|
|
New string `json:"New"`
|
|
}
|
|
if json.Unmarshal(raw, &td) == nil && td.Compound != "" {
|
|
t := s.Tyres[num]
|
|
t.Compound = td.Compound
|
|
t.New = td.New == "true" || td.New == "True"
|
|
s.Tyres[num] = t
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
case "TimingAppData":
|
|
var tad struct {
|
|
Lines map[string]json.RawMessage `json:"Lines"`
|
|
}
|
|
if json.Unmarshal(data, &tad) == nil {
|
|
for num, lineRaw := range tad.Lines {
|
|
var line struct {
|
|
Stints json.RawMessage `json:"Stints"`
|
|
}
|
|
if json.Unmarshal(lineRaw, &line) == nil && line.Stints != nil {
|
|
var driverStints []LiveStintData
|
|
for _, sRaw := range indexedRawValues(line.Stints) {
|
|
var st struct {
|
|
Compound string `json:"Compound"`
|
|
New string `json:"New"`
|
|
TotalLaps int `json:"TotalLaps"`
|
|
}
|
|
if json.Unmarshal(sRaw.Raw, &st) == nil && st.Compound != "" {
|
|
driverStints = append(driverStints, LiveStintData{
|
|
Compound: st.Compound,
|
|
New: st.New == "true" || st.New == "True",
|
|
Laps: st.TotalLaps,
|
|
})
|
|
}
|
|
}
|
|
if len(driverStints) > 0 {
|
|
s.Stints[num] = driverStints
|
|
lastStint := driverStints[len(driverStints)-1]
|
|
t := s.Tyres[num]
|
|
t.Age = lastStint.Laps
|
|
if lastStint.Compound != "" {
|
|
t.Compound = lastStint.Compound
|
|
t.New = lastStint.New
|
|
}
|
|
s.Tyres[num] = t
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case "TimingStats":
|
|
var ts struct {
|
|
Lines map[string]json.RawMessage `json:"Lines"`
|
|
}
|
|
if json.Unmarshal(data, &ts) == nil {
|
|
for num, lineRaw := range ts.Lines {
|
|
var line struct {
|
|
PersonalBestLapTime struct {
|
|
Value string `json:"Value"`
|
|
} `json:"PersonalBestLapTime"`
|
|
}
|
|
if json.Unmarshal(lineRaw, &line) == nil {
|
|
if d, ok := s.Drivers[num]; ok && line.PersonalBestLapTime.Value != "" {
|
|
d.BestLapTime = line.PersonalBestLapTime.Value
|
|
s.Drivers[num] = d
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if updated {
|
|
if baseTopic == "Position" {
|
|
s.positionUpdated = true
|
|
} else {
|
|
s.snapshotUpdated = true
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func (s *State) clearTransientFlags() {
|
|
s.positionUpdated = false
|
|
s.snapshotUpdated = false
|
|
}
|
|
|
|
func inflateTopicPayload(data json.RawMessage) (json.RawMessage, bool) {
|
|
var encoded string
|
|
if err := json.Unmarshal(data, &encoded); err != nil {
|
|
var wrapper struct {
|
|
Z string `json:"z"`
|
|
}
|
|
if json.Unmarshal(data, &wrapper) != nil || wrapper.Z == "" {
|
|
return nil, false
|
|
}
|
|
encoded = wrapper.Z
|
|
}
|
|
|
|
compressed, err := base64.StdEncoding.DecodeString(encoded)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
if inflated, ok := readCompressed(zlib.NewReader(bytes.NewReader(compressed))); ok {
|
|
return json.RawMessage(inflated), true
|
|
}
|
|
if inflated, ok := readCompressed(func() (io.ReadCloser, error) {
|
|
return flate.NewReader(bytes.NewReader(compressed)), nil
|
|
}()); ok {
|
|
return json.RawMessage(inflated), true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func readCompressed(r io.ReadCloser, err error) ([]byte, bool) {
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
defer r.Close()
|
|
out, err := io.ReadAll(r)
|
|
return out, err == nil
|
|
}
|
|
|
|
func (s *State) updateTeamRadio(data json.RawMessage) bool {
|
|
var payload struct {
|
|
Captures json.RawMessage `json:"Captures"`
|
|
}
|
|
if err := json.Unmarshal(data, &payload); err != nil {
|
|
log.Printf("live: skipping malformed TeamRadio payload: %v", err)
|
|
return false
|
|
}
|
|
if len(payload.Captures) == 0 || string(payload.Captures) == "null" {
|
|
log.Printf("live: skipping TeamRadio payload without Captures")
|
|
return false
|
|
}
|
|
|
|
captures := indexedRawValues(payload.Captures)
|
|
if len(captures) == 0 {
|
|
log.Printf("live: skipping TeamRadio payload with unexpected Captures shape")
|
|
return false
|
|
}
|
|
|
|
updated := false
|
|
for _, captureRaw := range captures {
|
|
var capture LiveRadioCapture
|
|
if err := json.Unmarshal(captureRaw.Raw, &capture); err != nil {
|
|
log.Printf("live: skipping malformed TeamRadio capture: %v", err)
|
|
continue
|
|
}
|
|
if capture.Utc == "" || capture.RacingNumber == "" || capture.Path == "" {
|
|
log.Printf("live: skipping incomplete TeamRadio capture: utc=%q racing_number=%q path=%q", capture.Utc, capture.RacingNumber, capture.Path)
|
|
continue
|
|
}
|
|
if s.hasTeamRadioCapture(capture) {
|
|
continue
|
|
}
|
|
s.TeamRadio = append(s.TeamRadio, capture)
|
|
updated = true
|
|
}
|
|
if updated {
|
|
sort.SliceStable(s.TeamRadio, func(i, j int) bool {
|
|
return s.TeamRadio[i].Utc < s.TeamRadio[j].Utc
|
|
})
|
|
if len(s.TeamRadio) > maxTeamRadioCaptures {
|
|
s.TeamRadio = append([]LiveRadioCapture(nil), s.TeamRadio[len(s.TeamRadio)-maxTeamRadioCaptures:]...)
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func (s *State) hasTeamRadioCapture(capture LiveRadioCapture) bool {
|
|
for _, existing := range s.TeamRadio {
|
|
if existing.Utc == capture.Utc && existing.RacingNumber == capture.RacingNumber && existing.Path == capture.Path {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *State) updatePositions(data json.RawMessage) bool {
|
|
var payload struct {
|
|
Position json.RawMessage `json:"Position"`
|
|
}
|
|
if json.Unmarshal(data, &payload) != nil || len(payload.Position) == 0 {
|
|
return false
|
|
}
|
|
|
|
updated := false
|
|
for _, sampleRaw := range indexedRawValues(payload.Position) {
|
|
var sample struct {
|
|
Entries map[string]struct {
|
|
Status string `json:"Status"`
|
|
X json.Number `json:"X"`
|
|
Y json.Number `json:"Y"`
|
|
Z json.Number `json:"Z"`
|
|
} `json:"Entries"`
|
|
}
|
|
if json.Unmarshal(sampleRaw.Raw, &sample) != nil {
|
|
continue
|
|
}
|
|
for num, entry := range sample.Entries {
|
|
x, okX := numberToFloat(entry.X)
|
|
y, okY := numberToFloat(entry.Y)
|
|
z, okZ := numberToFloat(entry.Z)
|
|
if !okX || !okY {
|
|
continue
|
|
}
|
|
if !okZ {
|
|
z = 0
|
|
}
|
|
s.Positions[num] = LivePositionData{
|
|
X: x,
|
|
Y: y,
|
|
Z: z,
|
|
Status: entry.Status,
|
|
}
|
|
updated = true
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func (s *State) updateTelemetry(data json.RawMessage) bool {
|
|
var payload struct {
|
|
Entries json.RawMessage `json:"Entries"`
|
|
}
|
|
if json.Unmarshal(data, &payload) != nil || len(payload.Entries) == 0 {
|
|
return false
|
|
}
|
|
|
|
updated := false
|
|
for _, entryRaw := range indexedRawValues(payload.Entries) {
|
|
var entry struct {
|
|
Cars map[string]struct {
|
|
Channels map[string]json.RawMessage `json:"Channels"`
|
|
} `json:"Cars"`
|
|
}
|
|
if json.Unmarshal(entryRaw.Raw, &entry) != nil {
|
|
continue
|
|
}
|
|
for num, car := range entry.Cars {
|
|
t := s.Telemetry[num]
|
|
if v, ok := channelInt(car.Channels, "0"); ok {
|
|
t.RPM = v
|
|
}
|
|
if v, ok := channelInt(car.Channels, "2"); ok {
|
|
t.Speed = v
|
|
}
|
|
if v, ok := channelInt(car.Channels, "3"); ok {
|
|
t.NGear = v
|
|
}
|
|
if v, ok := channelInt(car.Channels, "4"); ok {
|
|
t.Throttle = v
|
|
}
|
|
if v, ok := channelInt(car.Channels, "5"); ok {
|
|
t.Brake = v
|
|
}
|
|
if v, ok := channelInt(car.Channels, "45"); ok {
|
|
t.DRS = v
|
|
}
|
|
s.Telemetry[num] = t
|
|
updated = true
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func channelInt(channels map[string]json.RawMessage, key string) (int, bool) {
|
|
raw, ok := channels[key]
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
var n json.Number
|
|
if json.Unmarshal(raw, &n) == nil {
|
|
if i, err := n.Int64(); err == nil {
|
|
return int(i), true
|
|
}
|
|
if f, err := n.Float64(); err == nil {
|
|
return int(f), true
|
|
}
|
|
}
|
|
var s string
|
|
if json.Unmarshal(raw, &s) == nil {
|
|
var i int
|
|
if _, err := fmt.Sscanf(s, "%d", &i); err == nil {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func numberToFloat(n json.Number) (float64, bool) {
|
|
if n == "" {
|
|
return 0, false
|
|
}
|
|
v, err := n.Float64()
|
|
return v, err == nil
|
|
}
|
|
|
|
func updateDriver(drivers map[string]LiveDriverData, num string, line F1TimingLine) {
|
|
d, exists := drivers[num]
|
|
if !exists {
|
|
d = LiveDriverData{RacingNumber: num}
|
|
if line.RacingNumber != "" {
|
|
d.RacingNumber = line.RacingNumber
|
|
}
|
|
}
|
|
|
|
if line.Position != nil {
|
|
var newPos int
|
|
switch v := line.Position.(type) {
|
|
case string:
|
|
fmt.Sscanf(v, "%d", &newPos)
|
|
case float64:
|
|
newPos = int(v)
|
|
}
|
|
if newPos > 0 && newPos != d.Position {
|
|
d.PrevPosition = d.Position
|
|
d.Position = newPos
|
|
}
|
|
}
|
|
if line.GapToLeader != nil {
|
|
if s := extractStringVal(line.GapToLeader); s != "" {
|
|
d.GapToLeader = s
|
|
}
|
|
}
|
|
if line.IntervalToPositionAhead.Value != nil {
|
|
if s := extractStringVal(line.IntervalToPositionAhead.Value); s != "" {
|
|
d.Interval = s
|
|
}
|
|
}
|
|
if line.LastLapTime.Value != "" {
|
|
d.LastLapTime = line.LastLapTime.Value
|
|
d.LastLapPB = line.LastLapTime.PersonalFastest
|
|
d.LastLapOB = line.LastLapTime.OverallFastest
|
|
}
|
|
if line.BestLapTime.Value != "" {
|
|
d.BestLapTime = line.BestLapTime.Value
|
|
d.BestLapPB = line.BestLapTime.PersonalFastest
|
|
d.BestLapOB = line.BestLapTime.OverallFastest
|
|
if line.BestLapTime.Lap > 0 {
|
|
d.BestLapNum = line.BestLapTime.Lap
|
|
}
|
|
}
|
|
if line.InPit != nil {
|
|
d.InPit = toBool(line.InPit)
|
|
}
|
|
if line.PitOut != nil {
|
|
d.PitOut = toBool(line.PitOut)
|
|
}
|
|
if line.Retired != nil {
|
|
d.Retired = toBool(line.Retired)
|
|
}
|
|
if line.KnockedOut != nil {
|
|
d.KnockedOut = toBool(line.KnockedOut)
|
|
}
|
|
if line.Cutoff != nil {
|
|
d.Cutoff = toBool(line.Cutoff)
|
|
}
|
|
if line.NumberOfLaps != nil {
|
|
if v, ok := toInt(line.NumberOfLaps); ok {
|
|
d.NumberOfLaps = v
|
|
}
|
|
}
|
|
|
|
if st, ok := line.Speeds["ST"]; ok {
|
|
var sp struct {
|
|
Value string `json:"Value"`
|
|
}
|
|
if json.Unmarshal(st, &sp) == nil && sp.Value != "" {
|
|
d.SpeedTrap = sp.Value
|
|
}
|
|
}
|
|
|
|
for _, sector := range indexedRawValues(line.Sectors) {
|
|
i := sector.Index
|
|
if i >= 0 && i < 3 {
|
|
var sec struct {
|
|
Value string `json:"Value"`
|
|
PersonalFastest bool `json:"PersonalFastest"`
|
|
OverallFastest bool `json:"OverallFastest"`
|
|
}
|
|
if json.Unmarshal(sector.Raw, &sec) == nil {
|
|
if sec.Value == "" {
|
|
d.Sectors[i] = LiveSectorData{}
|
|
} else {
|
|
d.Sectors[i] = LiveSectorData{
|
|
Value: sec.Value,
|
|
PersonalFastest: sec.PersonalFastest,
|
|
OverallFastest: sec.OverallFastest,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
d.OnFlyingLap = !d.InPit && !d.Retired &&
|
|
(d.Sectors[0].Value != "" || d.Sectors[1].Value != "") &&
|
|
d.Sectors[2].Value == ""
|
|
|
|
drivers[num] = d
|
|
}
|
|
|
|
func extractStringVal(v interface{}) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
switch val := v.(type) {
|
|
case string:
|
|
return val
|
|
case float64:
|
|
if val == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("+%.3f", val)
|
|
case map[string]interface{}:
|
|
if s, ok := val["Value"].(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func toBool(v interface{}) bool {
|
|
switch val := v.(type) {
|
|
case bool:
|
|
return val
|
|
case string:
|
|
return val == "true" || val == "True"
|
|
}
|
|
return false
|
|
}
|
|
|
|
func toInt(v interface{}) (int, bool) {
|
|
switch val := v.(type) {
|
|
case float64:
|
|
return int(val), true
|
|
case json.Number:
|
|
if i, err := val.Int64(); err == nil {
|
|
return int(i), true
|
|
}
|
|
case string:
|
|
var i int
|
|
if _, err := fmt.Sscanf(val, "%d", &i); err == nil {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
type indexedRaw struct {
|
|
Index int
|
|
Raw json.RawMessage
|
|
}
|
|
|
|
func splitSignalRFrames(message []byte) []json.RawMessage {
|
|
parts := []json.RawMessage{}
|
|
start := 0
|
|
for i, b := range message {
|
|
if b != signalRRecordSeparator {
|
|
continue
|
|
}
|
|
if i > start {
|
|
parts = append(parts, json.RawMessage(message[start:i]))
|
|
}
|
|
start = i + 1
|
|
}
|
|
if start < len(message) {
|
|
parts = append(parts, json.RawMessage(message[start:]))
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func indexedRawValues(raw json.RawMessage) []indexedRaw {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return nil
|
|
}
|
|
|
|
var arr []json.RawMessage
|
|
if err := json.Unmarshal(raw, &arr); err == nil {
|
|
values := make([]indexedRaw, 0, len(arr))
|
|
for i, v := range arr {
|
|
values = append(values, indexedRaw{Index: i, Raw: v})
|
|
}
|
|
return values
|
|
}
|
|
|
|
var obj map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &obj); err == nil {
|
|
values := make([]indexedRaw, 0, len(obj))
|
|
for k, v := range obj {
|
|
i := 0
|
|
fmt.Sscanf(k, "%d", &i)
|
|
values = append(values, indexedRaw{Index: i, Raw: v})
|
|
}
|
|
sort.Slice(values, func(i, j int) bool {
|
|
return values[i].Index < values[j].Index
|
|
})
|
|
return values
|
|
}
|
|
|
|
return nil
|
|
}
|