Skip to content

Commit

Permalink
Feature add intial example (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
teamjorge authored Aug 19, 2024
1 parent b6a3e5b commit ca05386
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.telem
todo
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# ibt

IRacing Telemetry parsing and processing library
iRacing Telemetry parsing and processing library

![ibt logo](assets/ibt_mascot.png)


<!-- [![](https://img.shields.io/github/actions/workflow/status/spf13/cobra/test.yml?branch=main&longCache=true&label=Test&logo=github%20actions&logoColor=fff)](https://github.com/spf13/cobra/actions?query=workflow%3ATest) -->
[![Go Reference](https://pkg.go.dev/badge/github.com/teamjorge/ibt.svg)](https://pkg.go.dev/github.com/teamjorge/ibt)
[![Go Report Card](https://goreportcard.com/badge/github.com/teamjorge/ibt)](https://goreportcard.com/report/github.com/teamjorge/ibt)
[![codecov](https://codecov.io/gh/teamjorge/ibt/branch/main/graph/badge.svg?token=08QVKSEPXT)](https://codecov.io/gh/teamjorge/ibt)
Expand All @@ -13,12 +14,11 @@

`go get github.com/teamjorge/ibt`


## Overview

`ibt` is a package from parsing IRacing telemetry files. An *ibt* file is created when you enter the car and ends when you exit the car. By default, you can find these files in your `IRacing/telemetry/[car]/` directory. These files are binary for the most part, with the exception of the session data.
`ibt` is a package from parsing iRacing telemetry files. An *ibt* file is created when you enter the car and ends when you exit the car. By default, you can find these files in your `iRacing/telemetry/[car]/` directory. These files are binary for the most part, with the exception of the session data.

This package does not (yet) parse real-time as that requires opening a memory-mapped file and CGO. A planned real-time parsing package utilising this one will be available at some point.
This package will not parse real-time telemetry as that requires opening a memory-mapped file and CGO. A planned real-time parsing package leverage `ibt` will be available in the future.

## Features

Expand All @@ -28,3 +28,27 @@ This package does not (yet) parse real-time as that requires opening a memory-ma
* Great test coverage and code documentation.
* Freedom to use it your own way. Most of what is needed are public functions/methods.

## Examples

Please see the `examples` folder for detailed usage instructions.

To try the examples locally, please clone to repository:

```shell
git clone https://github.com/teamjorge/ibt
#or
git clone [email protected]:teamjorge/ibt.git

cd ibt
```

To run the example which summarises the track temperature per lap:

```shell
go run examples/track_temp/main.go

# Or to run it with your own telemetry files

go run examples/track_temp/main.go /path/to/telem/files/*ibt
```

Binary file added assets/ibt_mascot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions cmd/example/main.go

This file was deleted.

113 changes: 113 additions & 0 deletions examples/track_temp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"sort"

"github.com/teamjorge/ibt"
"github.com/teamjorge/ibt/headers"
"github.com/teamjorge/ibt/utilities"
"golang.org/x/exp/maps"
)

func main() {
flag.Parse()

// Use the default testing file if no ibt files were provided
// Provided files can use wildcards, for example ./telemetry/*.ibt
var filePattern string
if flag.Arg(0) == "" {
filePattern = ".testing/valid_test_file.ibt"
} else {
filePattern = flag.Arg(0)
}

// Find all files for parsing in case it had included a wildcard
files, err := filepath.Glob(filePattern)
if err != nil {
log.Fatalf("could not glob the given input files: %v", err)
}

// Parse the files into stubs
stubs, err := ibt.ParseStubs(files...)
if err != nil {
log.Fatalf("failed to parse stubs for %v. error - %v", files, err)
}

// Group the stubs into iRacing sessions
stubGroups := stubs.Group()

for _, stubGroup := range stubGroups {
for _, stub := range stubGroup {
stubFile, err := os.Open(stub.Filename())
if err != nil {
log.Fatalf("failed to open stub file %s for reading: %v", stub.Filename(), err)
}

// Create the instance(s) of your processor(s)
processor := NewTrackTempProcessor()

// Process the available telemetry for the ibt file. This is currently only utilising the Track Temp processor,
// but can include as many as you want.
if err := ibt.Process(stubFile, *stub.Headers(), processor); err != nil {
log.Fatalf("failed to process telemetry for stub %s: %v", stub.Filename(), err)
}

// Print the summarised track temperature
processor.Print()
}
}

}

type TrackTempProcessor struct {
tempMap map[int]float32
}

func NewTrackTempProcessor() *TrackTempProcessor {
t := new(TrackTempProcessor)

t.tempMap = make(map[int]float32)

return t
}

// Display name of the processor
func (t *TrackTempProcessor) Name() string { return "Track Temp" }

// Method used for processing every tick of telemetry
func (t *TrackTempProcessor) Process(input map[string]headers.VarHeader, hasNext bool, session *headers.Session) error {
TrackTempProcessor := input["TrackTempCrew"].Value.(float32)
lap := input["Lap"].Value.(int)

t.tempMap[lap] = TrackTempProcessor

return nil
}

// Utility function for create a result that can be joined with other processors.
//
// This will convert the results to map[int]interface{}, where the keys will refer to laps.
// Result is not yet required by any interfaces, but is useful when using multiple processors
// that summarise telemetry based by lap.
func (t *TrackTempProcessor) Result() map[int]interface{} {
return utilities.CreateGenericMap(t.tempMap)
}

// Columns required for the processor
func (t *TrackTempProcessor) Whitelist() []string { return []string{"Lap", "TrackTempCrew"} }

// Print the summarised Track Temperature
func (t *TrackTempProcessor) Print() {
fmt.Println("Track Temp:")
laps := maps.Keys(t.tempMap)
sort.Ints(laps)

for _, lap := range laps {
fmt.Printf("%03d - %.3f\n", lap, t.tempMap[lap])
}
}
2 changes: 1 addition & 1 deletion headers/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (s *Session) GetDriver() *Drivers {
return nil
}

// Session in which the ibt file occurred. This represents an actual IRacing session and not just information for a single ibt file.
// Session in which the ibt file occurred. This represents an actual iRacing session and not just information for a single ibt file.
type Session struct {
CameraInfo CameraInfo `yaml:"CameraInfo"`
CarSetup map[string]interface{} `yaml:"CarSetup"`
Expand Down
34 changes: 30 additions & 4 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ibt

import (
"github.com/teamjorge/ibt/headers"
"golang.org/x/exp/maps"
)

// Parser is used to iterate and process telemetry variables for a given ibt file and it's headers.
Expand All @@ -26,10 +27,8 @@ func NewParser(reader headers.Reader, header headers.Header, whitelist ...string
p := new(Parser)

p.reader = reader
p.whitelist = whitelist
if len(p.whitelist) == 0 || p.whitelist[0] == "*" {
p.whitelist = headers.AvailableVars(header.VarHeader())
}
p.whitelist = computeVars(header.VarHeader(), whitelist...)

p.length = header.TelemetryHeader().BufLen
p.bufferOffset = header.TelemetryHeader().BufOffset
p.varHeader = header.VarHeader()
Expand Down Expand Up @@ -80,3 +79,30 @@ func (p *Parser) read(start int) []byte {

return buf
}

// compareVars will retrieve vars when * is used and ensure a unique list
//
// Variables that are not found in the VarHeader will automatically be excluded.
func computeVars(vars map[string]headers.VarHeader, whitelist ...string) []string {
if len(whitelist) == 0 {
return headers.AvailableVars(vars)
}

for _, col := range whitelist {
if col == "*" {
return headers.AvailableVars(vars)
}
}

// de-duplicate the columns
varMap := make(map[string]struct{})

for _, col := range whitelist {
// ensure it's a valid column
if _, ok := vars[col]; ok {
varMap[col] = struct{}{}
}
}

return maps.Keys(varMap)
}
54 changes: 52 additions & 2 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"reflect"
"sort"
"testing"

"github.com/teamjorge/ibt/headers"
Expand Down Expand Up @@ -36,9 +37,11 @@ func TestParser(t *testing.T) {
t.Errorf("expected length to be %d, received: %d", 1072, p.length)
}

expectedWhitelist := []string{"Speed", "Lap"}
expectedWhitelist := []string{"Lap", "Speed"}
receivedWhitelist := p.whitelist
sort.Strings(receivedWhitelist)

if p.whitelist[0] != expectedWhitelist[0] || p.whitelist[1] != expectedWhitelist[1] {
if receivedWhitelist[0] != expectedWhitelist[0] || receivedWhitelist[1] != expectedWhitelist[1] {
t.Errorf("expected whitelist to be %v, received: %v", expectedWhitelist, p.whitelist)
}

Expand Down Expand Up @@ -167,3 +170,50 @@ func TestParserRead(t *testing.T) {
})

}

func TestCompareVars(t *testing.T) {
t.Run("computeVars() explicit columns", func(t *testing.T) {
vars := map[string]headers.VarHeader{
"var1": {},
"var2": {},
"var3": {},
"var4": {},
}

receivedVars := computeVars(vars, "var3", "test", "var4")
sort.Strings(receivedVars)

if receivedVars[0] != "var3" && receivedVars[1] != "var4" {
t.Errorf("expected vars to equal [%s, %s]. received: %v", "var3", "var4", receivedVars)
}
})

t.Run("computeVars() empty", func(t *testing.T) {
vars := map[string]headers.VarHeader{
"var1": {},
"var2": {},
}

receivedVars := computeVars(vars)
sort.Strings(receivedVars)

if receivedVars[0] != "var1" && receivedVars[1] != "var2" {
t.Errorf("expected vars to equal [%s, %s]. received: %v", "var1", "var2", receivedVars)
}
})

t.Run("computeVars() wildcard", func(t *testing.T) {
vars := map[string]headers.VarHeader{
"var1": {},
"var2": {},
"var3": {},
}

receivedVars := computeVars(vars, "*", "test", "*")
sort.Strings(receivedVars)

if receivedVars[0] != "var1" && receivedVars[1] != "var2" && receivedVars[2] != "var3" {
t.Errorf("expected vars to equal [%s, %s, %s]. received: %v", "var1", "var2", "var3", receivedVars)
}
})
}
4 changes: 2 additions & 2 deletions stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func parseStub(filename string) (Stub, error) {
return Stub{filename, header}, nil
}

// Group stubs together by their IRacing session.
// Group stubs together by their iRacing session.
//
// The process for grouping is slightly different for official and test sessions.
// Official sessions can utilise the SubSessionID, whereas Test sessions
Expand Down Expand Up @@ -111,7 +111,7 @@ func (stubs StubGroup) Group() []StubGroup {
return groups
}

// groupTestSessionStubs ensures that ibt files from IRacing Test sessions are grouped correctly.
// groupTestSessionStubs ensures that ibt files from iRacing Test sessions are grouped correctly.
//
// The logic for grouping Test session files is slightly different due to the lack of subSessionIds
// and rely on the ResultsPosition variable to determine start and end of a group.
Expand Down

0 comments on commit ca05386

Please sign in to comment.