Skip to content

Commit 4c06251

Browse files
committed
feat: support file based replay telemetry
1 parent 8e4b33f commit 4c06251

File tree

11 files changed

+627
-225
lines changed

11 files changed

+627
-225
lines changed

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ run: build
121121
run/live:
122122
@go run ${MAIN_PACKAGE_PATH}/main.go
123123

124+
## run/capture_replay: capture a replay and save to gt7-replay.gtz
125+
.PHONY: run/capture-replay
126+
run/capture-replay:
127+
@go run cmd/capture_replay/main.go
128+
@echo "Replay saved to gt7-replay.gtz"
129+
124130
## clean: clean up project and return to a pristine state
125131
.PHONY: clean
126132
clean:

README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ import telemetry_client "github.com/vwhitteron/gt-telemetry"
4545
Construct a new GT client and start reading the telemetry stream. All configuration fields are optional with the default values show in the example.
4646

4747
```go
48-
config := telemetry_client.Config{
49-
IPAddr: "255.255.255.255",
50-
LogLevel: "info",
48+
config := telemetry_client.GTClientOpts{
49+
Source: "udp://255.255.255.255:33739"
50+
LogLevel: "warn",
5151
StatsEnabled: false,
5252
VehicleDB: "./internal/vehicles/inventory.json",
5353
}
@@ -67,13 +67,43 @@ Read some data from the stream:
6767
)
6868
```
6969

70+
### Replay files ###
71+
72+
Offline saves of replay files can also be used to read in telemetry data. Files can be in either plain (`*.gtr`) or compressed (`*.gtz`) format.
73+
74+
Read telemetry from a replay file by setting the `Source` value in the `GTClientOpts` to a file URL, like so:
75+
76+
```go
77+
config := telemetry_client.GTClientOpts{
78+
Source: "file://examples/simple/replay.gtz"
79+
}
80+
```
81+
82+
#### Saving a replay to a file ####
83+
84+
Replays can be captured and saved to a file using `cmd/capture_replay/main.go`. Captures will be saved in plain or compressed formats according to the file extension as mentioned in the section above.
85+
86+
A replay can be saved to a default file by running:
87+
88+
```bash
89+
make run/capture-replay
90+
```
91+
92+
Alternatively, the replay can be captured to a compressed file with a different name and location by running:
93+
94+
```bash
95+
go run cmd/capture_replay/main.go -o /path/to/replay-file.gtz
96+
```
97+
7098
## Examples ##
7199

72-
The [examples](./examples) directory contains an example for accessing most data made available by the library. The telemetry data can be viewed by running:
100+
The [examples](./examples) directory contains example code for accessing most data made available by the library. The telemetry data from a sample saved replay can be viewed by running:
73101

74102
```bash
75103
make run/live
76104
```
77105

106+
The example code can also read live telemetry data from a PlayStation by removing the `Source` field in the `GTClientOpts`.
107+
78108
## Acknowledgements ##
79109
Special thanks to [Nenkai](https://github.com/Nenkai) for the excellent work documenting the Gran Turismo telemetry protocol.

cmd/capture_replay/main.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package main
2+
3+
import (
4+
"compress/gzip"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"log"
9+
"os"
10+
"time"
11+
12+
telemetry_client "github.com/vwhitteron/gt-telemetry"
13+
)
14+
15+
func main() {
16+
var outFile string
17+
18+
flag.StringVar(&outFile, "o", "gt7-replay.gtz", "Output file name. Default: gt7-replay.gtz")
19+
flag.Parse()
20+
21+
fh, err := os.Create(outFile)
22+
if err != nil {
23+
log.Fatal(err)
24+
}
25+
defer fh.Close()
26+
27+
var buffer io.Writer
28+
fileExt := outFile[len(outFile)-3:]
29+
switch fileExt {
30+
case "gtz":
31+
buffer, err = gzip.NewWriterLevel(fh, gzip.BestCompression)
32+
if err != nil {
33+
log.Fatal(err.Error())
34+
}
35+
buffer.(*gzip.Writer).Comment = "Gran Turismo 7 Telemetry Replay"
36+
case "gtr":
37+
buffer = fh
38+
default:
39+
os.Remove(outFile)
40+
log.Fatalf("Unsupported file extension %q, use either .gtr or .gtz", fileExt)
41+
}
42+
43+
gt, err := telemetry_client.NewGTClient(telemetry_client.GTClientOpts{})
44+
if err != nil {
45+
fmt.Println("Error creating GT client: ", err)
46+
os.Exit(1)
47+
}
48+
49+
go gt.Run()
50+
51+
fmt.Println("Waiting for replay to start")
52+
53+
framesCaptured := -1
54+
lastTimeOfDay := time.Duration(0)
55+
sequenceID := ^uint32(0)
56+
startTime := time.Duration(0)
57+
diff := uint32(0)
58+
for {
59+
// ignore packets that have aldready been processed
60+
if sequenceID == gt.Telemetry.SequenceID() {
61+
timer := time.NewTimer(4 * time.Millisecond)
62+
<-timer.C
63+
continue
64+
}
65+
66+
diff = gt.Telemetry.SequenceID() - sequenceID
67+
sequenceID = gt.Telemetry.SequenceID()
68+
69+
// Set the last time seen when the first frame is received
70+
if lastTimeOfDay == time.Duration(0) {
71+
lastTimeOfDay = gt.Telemetry.TimeOfDay()
72+
continue
73+
}
74+
75+
// Finish recording when the replay restarts
76+
if gt.Telemetry.TimeOfDay() <= startTime {
77+
// The time of day sometimes flaps in the first few frames
78+
if framesCaptured < 60 {
79+
continue
80+
}
81+
82+
fmt.Println("Replay restart detected")
83+
if b, ok := buffer.(*gzip.Writer); ok {
84+
if err := b.Flush(); err != nil {
85+
log.Fatal(err)
86+
}
87+
}
88+
break
89+
}
90+
91+
// Start recording when the replay starts
92+
if framesCaptured == -1 && gt.Telemetry.TimeOfDay() != lastTimeOfDay {
93+
fmt.Printf("Starting capture, frame size: %d bytes\n", len(gt.DecipheredPacket))
94+
95+
startTime = gt.Telemetry.TimeOfDay()
96+
framesCaptured = 0
97+
98+
extraData := fmt.Sprintf("Time of day: %+v, Manufacturer: %s, Model: %s",
99+
startTime,
100+
gt.Telemetry.VehicleManufacturer(),
101+
gt.Telemetry.VehicleModel(),
102+
)
103+
if b, ok := buffer.(*gzip.Writer); ok {
104+
b.Extra = []byte(extraData)
105+
}
106+
107+
fmt.Println(extraData)
108+
} else {
109+
time.Sleep(4 * time.Millisecond)
110+
}
111+
112+
// Write the frame to the gzip buffer
113+
if framesCaptured >= 0 {
114+
if diff > 1 {
115+
fmt.Printf("Dropped %d frames\n", diff-1)
116+
}
117+
118+
_, err := buffer.Write(gt.DecipheredPacket)
119+
if err != nil {
120+
log.Fatal(err)
121+
}
122+
123+
framesCaptured++
124+
lastTimeOfDay = gt.Telemetry.TimeOfDay()
125+
}
126+
127+
timer := time.NewTimer(4 * time.Millisecond)
128+
<-timer.C
129+
130+
if framesCaptured%300 == 0 {
131+
fmt.Printf("%d frames captured\n", framesCaptured)
132+
}
133+
}
134+
135+
fmt.Printf("Capture complete, total frames: %d\n", framesCaptured)
136+
}

examples/simple/main.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
func main() {
1212
clientConfig := telemetry_client.GTClientOpts{
13+
Source: "file://examples/simple/replay.gtz",
1314
StatsEnabled: true,
1415
}
1516

@@ -24,6 +25,10 @@ func main() {
2425

2526
sequenceID := uint32(0)
2627
for {
28+
if client.Finished {
29+
break
30+
}
31+
2732
if sequenceID == client.Telemetry.SequenceID() {
2833
time.Sleep(8 * time.Millisecond)
2934
continue
@@ -39,18 +44,12 @@ func main() {
3944
hasTurbo := client.Telemetry.Flags().HasTurbo
4045
boostStr := ""
4146
if hasTurbo {
42-
boostStr = fmt.Sprintf("Boost: %1.02f Bar", client.Telemetry.TurboBoostBar())
47+
boostStr = fmt.Sprintf("Boost: %+1.02f Bar", client.Telemetry.TurboBoostBar())
4348
}
4449

4550
fmt.Print("\033[H\033[2J")
46-
fmt.Printf("Sequence ID: %d %s\nTime of day: %+v\n",
51+
fmt.Printf("Sequence ID: %d\nTime of day: %+v\n",
4752
client.Telemetry.SequenceID(),
48-
renderFlag(
49-
client.Statistics.Heartbeat,
50-
"Heartbeat",
51-
"yellow",
52-
"invisible",
53-
),
5453
client.Telemetry.TimeOfDay(),
5554
)
5655
fmt.Printf("Race Lap: %d of %d Last lap: %+v Best lap: %+v Start position: %d Race entrants: %d\n",
@@ -95,7 +94,7 @@ func main() {
9594
client.Telemetry.OilPressureKPA(),
9695
boostStr,
9796
)
98-
fmt.Printf("Clutch Position: %0.0f%% Engagement: %0.0f%% Output: %5.0f RPM\n",
97+
fmt.Printf("Clutch Position: %3.0f%% Engagement: %3.0f%% Output: %5.0f RPM\n",
9998
client.Telemetry.ClutchActuationPercent(),
10099
client.Telemetry.ClutchEngagementPercent(),
101100
client.Telemetry.ClutchOutputRPM(),
@@ -226,7 +225,8 @@ func main() {
226225
)
227226
}
228227

229-
time.Sleep(64 * time.Millisecond)
228+
timer := time.NewTimer(64 * time.Millisecond)
229+
<-timer.C
230230
}
231231
}
232232

examples/simple/replay.gtz

945 KB
Binary file not shown.

0 commit comments

Comments
 (0)