package main import ( "bytes" "encoding/binary" "fmt" "log" "os" "os/signal" "strings" "sync" "syscall" "time" // "github.com/tarm/serial" ) type GameSource interface { GetData(string) (interface{}, error) UpdateData() error GetSessionInfo() (interface{}, error) } type SimulationData struct { Speed int32 Gear int32 RPM int32 LapCount int32 LapDistPct float32 LapTime [16]byte // Current lap time LapDelta [16]byte // Delta to selected reference lap BestLapTime [16]byte // Best lap in session LastLapTime [16]byte // Last lap time FuelUsageCurLap float32 FuelPerLap float32 FuelPct float32 FuelLiters float32 FuelTotal float32 // This will be calculated and passed in Liters Position int32 Standings [5]StandingsLine } var ( paddingStandingsLine = StandingsLine{ DriverName: createDriverName("---"), Lap: 0, CarIdx: 0, LapPct: 0, EstTime: 0, TimeBehind: 0, } iniFuelLvl float32 fuelLevels map[int]float32 lastMessageTime time.Time mu sync.Mutex ) func msToKph(v float32) int { return int((3600 * v) / 1000) } func lapTimeRepresentation(t float32) string { if t < 0 { t = 0 } wholeSeconds := int64(t) lapTime := time.Unix(wholeSeconds, int64((t-float32(wholeSeconds))*1e9)) return lapTime.Format("04:05.000") } func lapTimeDeltaRepresentation(t float32) string { sign := '-' if t < 0 { sign = '+' t = -1 * t } if t >= 1 { wholeSeconds := int64(t) lapTime := time.Unix(wholeSeconds, int64((t-float32(wholeSeconds))*1e9)) return fmt.Sprintf("%c%s", sign, lapTime.Format("5.00")) } return fmt.Sprintf("%c.%02d", sign, int64(t*100)) } func resetTerminal() { // Show cursor and clear screen fmt.Print("\033[?25h\033[2J\033[H") } func (e *ESDI) setupSignalHandlers() chan string { sigc := make(chan os.Signal, 1) signal.Notify(sigc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, ) done := make(chan string) go func() { s := <-sigc switch s { case syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGHUP: resetTerminal() } close(done) e.Close() }() return done } // func sendData(e *ESDI, s *serial.Port, done <-chan string, errC chan string) { // ticker := time.NewTicker(time.Millisecond * 25) // defer ticker.Stop() // // for { // select { // case <-done: // return // case <-ticker.C: // mu.Lock() // // data := DataToSend{ // Speed: e.data.Speed, // Gear: e.data.Gear, // RPM: e.data.RPM, // LapCount: e.data.LapCount, // LapDistPct: e.data.LapDistPct, // // FuelPerLap: e.data.FuelPerLap, // // FuelUsageCurLap: e.data.FuelUsageCurLap, // } // // copy(data.LapTime[:], e.data.LapTime[:]) // copy(data.LapDelta[:], e.data.LapDelta[:]) // copy(data.BestLapTime[:], e.data.BestLapTime[:]) // copy(data.LastLapTime[:], e.data.LastLapTime[:]) // // var buf bytes.Buffer // err := binary.Write(&buf, binary.LittleEndian, data) // // _, err = s.Write(buf.Bytes()) // if err != nil { // log.Printf("Unable to write data: %v", err) // break // } // // lastMessageTime = time.Now() // mu.Unlock() // } // } // } func printData(e *ESDI, done <-chan string) { ticker := time.NewTicker(time.Millisecond * 25) defer ticker.Stop() for { select { case <-done: return case <-ticker.C: var buffer strings.Builder buffer.WriteString("\033[?25l\033[2J\033[H") mu.Lock() // buffer.WriteString("Car data:\n") buffer.WriteString(fmt.Sprintf("Gear: %d, RPM: %d, Speed: %d\n\n", e.dataPacket.Gear, e.dataPacket.RPM, e.dataPacket.Speed)) // // buffer.WriteString("Fuel data:\n") // buffer.WriteString(fmt.Sprintf("Fuel: %sL/%sL %s/lap [%s%%]\n\n", e.dataPacket.FuelLiters, // e.dataPacket.FuelTotal, e.dataPacket.FuelPerLap, e.dataPacket.FuelPct)) // // buffer.WriteString("Lap data:\n") // buffer.WriteString(fmt.Sprintf("LapTime: %s [%s]\n", e.dataPacket.LapTime, // e.dataPacket.LapDelta)) // buffer.WriteString(fmt.Sprintf("Best Lap Time: %s\n", e.dataPacket.BestLapTime)) // buffer.WriteString(fmt.Sprintf("Last Lap Time: %s\n", e.dataPacket.LastLapTime)) // buffer.WriteString(fmt.Sprintf("Lap: %d [%.2f%%]\n\n", e.dataPacket.LapCount, // e.data.LapDistPct)) // // buffer.WriteString("Position data:\n") // buffer.WriteString(fmt.Sprintf("Pos: %d\n", e.dataPacket.Position)) for p, v := range e.dataPacket.Standings { s := fmt.Sprintf("[%2d] %s %-16s %-16s\n", p+1, v.Lap, string(bytes.Trim(v.DriverName[:], "\x00")), v.TimeBehindString) buffer.WriteString(s) } mu.Unlock() // buffer.WriteString("\n" + message) fmt.Print(buffer.String()) } } } func (e *ESDI) getVehicleData() { curGear := e.irsdk.Vars.Vars["Gear"].Value curRPM := e.irsdk.Vars.Vars["RPM"].Value curSpeed := e.irsdk.Vars.Vars["Speed"].Value mu.Lock() e.data.Gear = int32(curGear.(int)) e.data.RPM = int32(curRPM.(float32)) e.data.Speed = int32(msToKph(curSpeed.(float32))) mu.Unlock() } func (e *ESDI) fuelData() { fLiters := e.irsdk.Vars.Vars["FuelLevel"].Value fPct := e.irsdk.Vars.Vars["FuelLevelPct"].Value curLap := e.irsdk.Vars.Vars["Lap"].Value.(int) fuelLevels[curLap] = fLiters.(float32) if curLap-2 < 0 { e.data.FuelPerLap = 0.0 } else { e.data.FuelPerLap = fuelLevels[curLap-2] - fuelLevels[curLap-1] } mu.Lock() e.data.FuelPct = float32(fPct.(float32)) * 100 e.data.FuelLiters = float32(fLiters.(float32)) e.data.FuelTotal = (100 * e.data.FuelLiters) / e.data.FuelPct mu.Unlock() } func (e *ESDI) lapData() { currentLap := e.irsdk.Vars.Vars["Lap"].Value lapDistPct := e.irsdk.Vars.Vars["LapDistPct"].Value currentLapTime := e.irsdk.Vars.Vars["LapCurrentLapTime"].Value lapBestLapTime := e.irsdk.Vars.Vars["LapBestLapTime"].Value lapLastLapTime := e.irsdk.Vars.Vars["LapLastLapTime"].Value lapDeltaToBestLap := e.irsdk.Vars.Vars["LapDeltaToBestLap"].Value mu.Lock() e.data.LapCount = int32(currentLap.(int)) e.data.LapDistPct = float32(lapDistPct.(float32)) * 100 copy(e.data.LapTime[:], string(lapTimeRepresentation(currentLapTime.(float32)))) copy(e.data.LastLapTime[:], string(lapTimeRepresentation(lapLastLapTime.(float32)))) copy(e.data.BestLapTime[:], string(lapTimeRepresentation(lapBestLapTime.(float32)))) copy(e.data.LapDelta[:], string(lapTimeDeltaRepresentation(lapDeltaToBestLap.(float32)))) mu.Unlock() } func (e *ESDI) positionData() { standings := createStandingsTable(e.irsdk) relativeStandings(e.irsdk, standings, e.irsdk.SessionInfo.DriverInfo.DriverCarIdx) p := findEntry(standings, func(l StandingsLine) bool { return l.CarIdx == int32(e.irsdk.SessionInfo.DriverInfo.DriverCarIdx) }) lowerLim := p - 2 upperLim := p + 3 var lowerPadding []StandingsLine var upperPadding []StandingsLine if lowerLim < 0 { lowerPadding = make([]StandingsLine, abs(lowerLim)) for k := 0; k < abs(lowerLim); k++ { lowerPadding[k] = paddingStandingsLine } lowerLim = 0 } if upperLim >= len(standings) { upperPadding = make([]StandingsLine, upperLim-len(standings)) for k := 0; k < upperLim-len(standings); k++ { upperPadding[k] = paddingStandingsLine } upperLim = len(standings) } standings = append(lowerPadding, standings[lowerLim:upperLim]...) standings = append(standings, upperPadding...) mu.Lock() copy(e.data.Standings[:], standings[0:5]) e.data.Position = int32(p) mu.Unlock() } type StandingsLineDataPacket struct { Lap [4]byte DriverName [16]byte TimeBehindString [16]byte } type DataPacket struct { Speed int32 Gear int32 RPM int32 // LapCount int32 // LapTime [10]byte // Current lap time // LapDelta [10]byte // Delta to selected reference lap // BestLapTime [10]byte // Best lap in session // LastLapTime [10]byte // Last lap time // FuelPerLap [8]byte // FuelPct [8]byte // FuelLiters [8]byte // FuelTotal [8]byte // This will be calculated and passed in Liters // Position int32 Standings [5]StandingsLineDataPacket } func packageStandingsLineDataPacket(data [5]StandingsLine) [5]StandingsLineDataPacket { var sl [5]StandingsLineDataPacket for k := range data { copy(sl[k].Lap[:], []byte(fmt.Sprintf("%-3d", data[k].Lap))) copy(sl[k].DriverName[:], []byte(fmt.Sprintf("%-16s", data[k].DriverName[0:16]))) copy(sl[k].TimeBehindString[:], []byte(fmt.Sprintf("%-16s", data[k].TimeBehindString[0:7]))) sl[k].TimeBehindString[7] = '\x00' sl[k].DriverName[15] = '\x00' } return sl } func (e *ESDI) telemetry() { // Set the handlers done := e.setupSignalHandlers() dataError := make(chan string) // go sendData(e, e.SerialConn, done, dataError) go printData(e, done) mainLoopTicker := time.NewTicker(time.Second / 240) defer mainLoopTicker.Stop() fuelLevels = make(map[int]float32, 256) for { select { case s := <-done: resetTerminal() fmt.Println(s) return case s := <-dataError: done <- s case <-mainLoopTicker.C: var err error _, err = e.irsdk.Update(time.Millisecond * 100) if err != nil { fmt.Printf("could not update data: %v", err) continue } e.getVehicleData() e.fuelData() e.lapData() e.positionData() mu.Lock() // Test the actual dataPacket we are sending over the wire e.dataPacket.Speed = e.data.Speed e.dataPacket.Gear = e.data.Gear e.dataPacket.RPM = e.data.RPM // e.dataPacket.LapCount = e.data.LapCount // e.dataPacket.Position = e.data.Position e.dataPacket.Standings = packageStandingsLineDataPacket(e.data.Standings) // copy(e.dataPacket.FuelPerLap[:], []byte(fmt.Sprintf("%.2f", e.data.FuelPerLap))) // copy(e.dataPacket.FuelPct[:], []byte(fmt.Sprintf("%.2f", e.data.FuelPct))) // copy(e.dataPacket.FuelTotal[:], []byte(fmt.Sprintf("%.2f", e.data.FuelTotal))) // copy(e.dataPacket.FuelLiters[:], []byte(fmt.Sprintf("%.2f", e.data.FuelLiters))) // // copy(e.dataPacket.LapTime[:], e.data.LapTime[:]) // copy(e.dataPacket.LapDelta[:], e.data.LapDelta[:]) // copy(e.dataPacket.BestLapTime[:], e.data.BestLapTime[:]) // copy(e.dataPacket.LastLapTime[:], e.data.LastLapTime[:]) var buf bytes.Buffer err = binary.Write(&buf, binary.LittleEndian, e.dataPacket) _, err = e.SerialConn.Write(buf.Bytes()) if err != nil { log.Printf("Unable to write data: %v", err) break } lastMessageTime = time.Now() mu.Unlock() } } }