Created
June 1, 2024 21:29
-
-
Save clarkb7/911721ede4f8c9bb1d82abbce3dce7bd to your computer and use it in GitHub Desktop.
Proof of Concept for parsing Assetto Corsa tc files from the ctelemetry directory. Can output JSON or display a graph with matplotlib.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
This script is a proof of concept for parsing the Assetto Corsa tc files found in the ctelemetry directory ("%USERPROFILE%\Documents\Assetto Corsa\ctelemetry\player") | |
Usage: python ac_tc_reader.py [--json] [--graph] filename | |
--json Output the telemetry data in a JSON format | |
--graph Graph the telemetry data. Requires matplotlib to be installed. | |
File format: | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+ | |
| 4 bytes | 4 bytes | sizeOfName bytes | 4 bytes | sizeOfTrack bytes | | |
| unknown | sizeOfName | Name string | sizeOfTrackName | TrackName string | | |
| | (integer) | (UTF-8 string) | (integer) | (UTF-8 string) | | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+ | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+-------------------+ | |
| 4 bytes | sizeOfCar bytes | 4 bytes | sizeOfTrackVariation bytes | 4 bytes | 4 bytes | | |
| sizeOfCar | CarName string | sizeOfTrackVariation | TrackVariation string | LapTime (milliseconds) | numDataPoints | | |
| (integer) | (UTF-8 string) | (integer) | (UTF-8 string) | (integer) | (integer) | | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+-------------------+ | |
+-------------------+---------------------+------------------+-------------------+-----------------+ | |
| 4 bytes | 4 bytes | 4 bytes | 4 bytes | 4 bytes | | |
| Gear | Position | Speed (km/h) | Throttle | Brake | | |
| (integer) | (float) | (float) | (float) | (float) | | |
+-------------------+---------------------+------------------+-------------------+-----------------+ | |
| ... | ... | ... | ... | ... | | |
| Repeat for each datapoint (total numDataPoints times) | | |
+-------------------+---------------------+------------------+-------------------+-----------------+ | |
floats are in IEEE 754 format. | |
Gear: | |
- offset by one, AC displays gear 4 for a value of 5 | |
- I don't know how neutral or reverse are represented. | |
Position: | |
- monotonically increases from 0 to 1, used as x axis in graph | |
- graphs linear when compared to numDataPoints, so other graphs look the same, too, if you use numDataPoints as x axis instead | |
- not sure if it's actually related to your position in the lap or not, what happens if you drive backwards for a bit? | |
- seems like AC might combine this value with the track length to show the `Km:` value in the bottom right of screen | |
Speed: | |
- unit is km/h | |
Throttle: | |
- value 0 (no throttle input) to 1.0 (max throttle input) | |
Brake: | |
- value 0 (no brake input) to 1.0 (max brake input) | |
Every .tc file seems to be the same size and have the same number of datapoints (3999), regardless of track length or lap time. | |
I'm not sure if this means that the sampling is normalized (longer tracks/laptime less samples, shorter tracks/laptime more samples), | |
or if the sample frequency is static and stored in a circular buffer, so that old data is lost. | |
Assetto Corsa displays a `Km` value that goes from 0 to track length, which leads me to believe that it is normalized. | |
The output JSON format is: | |
{ | |
'user': string, | |
'track': string, | |
'track_variation': string, | |
'car': string, | |
'lap_time_ms': integer, | |
'lap_time': string, | |
'num_datapoints': integer, | |
'position': [float], | |
'gear': [float], | |
'speed': [float], | |
'throttle': [float], | |
'brake': [float], | |
} | |
''' | |
import struct | |
import sys | |
import datetime | |
import argparse | |
import json | |
from collections import namedtuple | |
def read_var_bytes(fstream): | |
""" | |
Unpacks a byte string in form | |
+-------------------+------------------------+ | |
| 4 bytes | $size bytes | | |
| size | string | | |
| (integer) | (UTF-8 string) | | |
+-------------------+------------------------+ | |
string is not null terminated | |
""" | |
size, = struct.unpack("<I", fstream.read(4)) | |
if size == 0: | |
return b"" | |
s = fstream.read(size) | |
return s | |
DataPoint = namedtuple('DataPoint', ['gear', 'position', 'speed', 'throttle', 'brake']) | |
def read_datapoint(fstream): | |
""" | |
Unpacks 20 byte datapoint with layout | |
+-------------------+---------------------+------------------+-------------------+-----------------+ | |
| 4 bytes | 4 bytes | 4 bytes | 4 bytes | 4 bytes | | |
| Gear | Position | Speed | Throttle | Brake | | |
| (integer) | (float) | (float) | (float) | (float) | | |
+-------------------+---------------------+------------------+-------------------+-----------------+ | |
floats are in IEEE 754 format. | |
""" | |
datapoint = struct.unpack('Iffff', fstream.read(20)) | |
return DataPoint(*datapoint) | |
def read_tc_header(fstream): | |
""" | |
Unpacks the header of the tc file, which contains player name, track name, car name, track variation, and lap time. | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+ | |
| 4 bytes | 4 bytes | sizeOfName bytes | 4 bytes | sizeOfTrack bytes | | |
| unknown | sizeOfName | Name string | sizeOfTrackName | TrackName string | | |
| | (integer) | (UTF-8 string) | (integer) | (UTF-8 string) | | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+ | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+-------------------+ | |
| 4 bytes | sizeOfCar bytes | 4 bytes | sizeOfTrackVariation bytes | 4 bytes | 4 bytes | | |
| sizeOfCar | CarName string | sizeOfTrackVariation | TrackVariation string | LapTime (milliseconds) | numDataPoints | | |
| (integer) | (UTF-8 string) | (integer) | (UTF-8 string) | (integer) | (integer) | | |
+-------------------+-------------------+-----------------------+----------------------------+------------------------+-------------------+ | |
""" | |
# unknown header | |
_ = fstream.read(4) | |
user = read_var_bytes(fstream).decode('utf-8') | |
track = read_var_bytes(fstream).decode('utf-8') | |
car = read_var_bytes(fstream).decode('utf-8') | |
track_variation = read_var_bytes(fstream).decode('utf-8') | |
lap_time_ms = struct.unpack('I', fstream.read(4))[0] | |
num_datapoints = struct.unpack('I', fstream.read(4))[0] | |
return { | |
'user': user, | |
'track': track, | |
'track_variation': track_variation, | |
'car': car, | |
'lap_time_ms': lap_time_ms, | |
'num_datapoints': num_datapoints, | |
} | |
def read_datapoints(fstream, num_datapoints): | |
for _ in range(num_datapoints): | |
datapoint = read_datapoint(fstream) | |
yield datapoint | |
def as_dict(header, datapoints): | |
lap_time = str(datetime.timedelta(milliseconds=header['lap_time_ms']))[:-3] | |
return { | |
'user': header['user'], | |
'track': header['track'], | |
'track_variation': header['track_variation'], | |
'car': header['car'], | |
'lap_time_ms': header['lap_time_ms'], | |
'lap_time': lap_time, | |
'num_datapoints': header['num_datapoints'], | |
'position': [dp.position for dp in datapoints], | |
'gear': [dp.gear for dp in datapoints], | |
'speed': [dp.speed for dp in datapoints], | |
'throttle': [dp.throttle for dp in datapoints], | |
'brake': [dp.brake for dp in datapoints], | |
} | |
def show_json(jso): | |
s = json.dumps(jso) | |
print(s) | |
def graph_data(data): | |
import matplotlib.pyplot as plt | |
# Extract data points | |
lap_time = data['lap_time'] | |
num_data_points = data['num_datapoints'] | |
position = data['position'] | |
# AC shows gear at an offset | |
gear = [gear-1 for gear in data['gear']] | |
speed = data['speed'] | |
throttle = data['throttle'] | |
brake = data['brake'] | |
# Create time points for the x-axis | |
time_points = list(range(num_data_points)) | |
x_axis = time_points | |
graphs = [ | |
{ | |
'label': 'Brake', | |
'data': brake, | |
'ylabel': 'Brake', | |
'title': 'Brake over time', | |
'color': 'c', | |
}, | |
{ | |
'label': 'Speed', | |
'data': speed, | |
'ylabel': 'Speed', | |
'title': 'Speed over time', | |
'color': 'r', | |
}, | |
{ | |
'label': 'Throttle', | |
'data': throttle, | |
'ylabel': 'Throttle', | |
'title': 'Throttle over time', | |
'color': 'm', | |
}, | |
{ | |
'label': 'Gear', | |
'data': gear, | |
'ylabel': 'Gear', | |
'title': 'Gear over time', | |
'color': 'b', | |
}, | |
# { | |
# 'label': 'Position', | |
# 'data': position, | |
# 'ylabel': 'Position', | |
# 'title': 'Position over time', | |
# 'color': 'g', | |
# }, | |
] | |
fig, axs = plt.subplots(len(graphs), 1, figsize=(10, 20), sharex=True) | |
for i in range(len(graphs)): | |
g = graphs[i] | |
ax = axs[i] | |
ax.plot(x_axis, g['data'], label=g['label'], color=g['color']) | |
ax.set_ylabel(g['ylabel']) | |
ax.set_title(g['title']) | |
ax.grid(True) | |
plt.tight_layout() | |
plt.subplots_adjust(top=0.95, bottom=0.15, left=0.1, right=0.95, hspace=0.5) | |
# Add label for user, car, track, and track variation | |
info = fig.text(0.5, .03, f"User: {data['user']} | Car: {data['car']} | Track: {data['track']} | Track Variation: {data['track_variation']}", | |
ha='center', | |
bbox=dict(facecolor='lightblue', alpha=0.5)) | |
info.set_fontfamily(['monospace']) | |
# Add label for displaying the values on mouseover | |
mouseoverValues = fig.text(0.5, .06, "", | |
ha='center', | |
bbox=dict(facecolor='lightblue', alpha=0.5)) | |
mouseoverValues.set_fontfamily(['monospace']) | |
mouseoverValues.set_visible(False) | |
# Add vertical line on each graph to track mouse | |
vlines = [] | |
for ax in axs: | |
vline = ax.axvline(x=0, color='gray', linestyle='--', alpha=0.5) | |
vlines.append(vline) | |
# Update label and vertical line on hover | |
def update_hover(event): | |
if not event.inaxes: | |
return | |
x, y = event.xdata, event.ydata | |
index = int(x) | |
if not (0 <= index < num_data_points): | |
return | |
# Fomat values for each graph for display in mouseoverValues box | |
index_str = f'{index:<6}' | |
lap_time_str = f'{lap_time:<6}' | |
speed_str = f"{speed[index]:>6.2f} Km/h" | |
throttle_str = f"{throttle[index]:<4.0%}" | |
brake_str = f"{brake[index]:<4.0%}" | |
gear_str = f"{gear[index]:<3}" | |
position_str = f"{position[index]:<12.3f}" | |
text = f"Index: {index_str} Lap Time: {lap_time_str} Speed: {speed_str} Throttle: {throttle_str} Brake: {brake_str} Gear: {gear_str} Position: {position_str} " | |
mouseoverValues.set_text(text) | |
mouseoverValues.set_visible(True) | |
# Set x value of vertical line | |
for vline in vlines: | |
vline.set_xdata([x]) | |
fig.canvas.draw_idle() | |
# When mouse is in a graph, display mouseoverValues and vertical lines | |
# clear when mouse not in graph | |
def hover(event): | |
if event.inaxes: | |
update_hover(event) | |
else: | |
mouseoverValues.set_visible(False) | |
for vline in vlines: | |
vline.set_xdata([0]) | |
fig.canvas.draw_idle() | |
fig.canvas.mpl_connect("motion_notify_event", hover) | |
plt.show() | |
def main(argv): | |
parser = argparse.ArgumentParser() | |
parser.add_argument('filename') | |
parser.add_argument('--json', action='store_true', | |
help="Output the telemetry data in a JSON format") | |
parser.add_argument('--graph', action='store_true', | |
help="Graph the telemetry data. Requires matplotlib to be installed.") | |
args = parser.parse_args(argv) | |
f = open(args.filename, 'rb') | |
header = read_tc_header(f) | |
datapoints = list(read_datapoints(f, header['num_datapoints'])) | |
data = as_dict(header, datapoints) | |
if args.json: | |
show_json(data) | |
if args.graph: | |
graph_data(data) | |
if __name__ == "__main__": | |
main(sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample matplotlib graph