PDA

View Full Version : Capturing and reading F1 UDP telemetry



Nizars
30th November 2017, 17:55
Hi all!

I am back again with a new project.

The concept:
I am making an overlay for a F1 game to use on my Twitch channel. The game has a UDP telemetry feature that can be activated to provide live game information to racing gear equipment and monitoring apps that players can use. The data is transmitted locally, or to an IP, and a port that is provided to the game by the user.
12710

Here is the information that I have about the data being transmitted:

UDP Packet Structure:
The data is sent as raw data in the UDP packet, converted to a char array, with packing enabled (no padding to align different sized types). To decode this into something usable it should be a case of casting the packet data back to the UDPPacket struct (or another structure with the same layout). The layout of the UDP data is as follows:



// Packet size – 1289 bytes

struct UDPPacket
{
float m_time;
float m_lapTime;
float m_lapDistance;
float m_totalDistance;
float m_x; // World space position
float m_y; // World space position
float m_z; // World space position
float m_speed; // Speed of car in MPH
...
float m_yd; // World space forward direction
float m_zd; // World space forward direction
float m_susp_pos[4]; // Note: All wheel arrays have the order:
float m_susp_vel[4]; // RL, RR, FL, FR
float m_wheel_speed[4];
...
float m_gforce_lat;
float m_gforce_lon;
...
byte m_rev_lights_percent; // NEW: rev lights indicator (percentage)
byte m_is_spectating; // NEW: whether the player is spectating
byte m_spectator_car_index; // NEW: index of the car being spectated


// Car data
byte m_num_cars; // number of cars in data
byte m_player_car_index; // index of player's car in the array
...
CarUDPData m_car_data[20]; // data for all cars on track
float m_ang_acc_y; // NEW (v1.8) angular acceleration y-component
float m_ang_acc_z; // NEW (v1.8) angular acceleration z-component
};

struct CarUDPData
{
float m_worldPosition[3]; // world co-ordinates of vehicle
float m_lastLapTime;
byte m_driverId;
byte m_teamId;
...
byte m_currentLapInvalid; // current lap invalid - 0 = valid, 1 = invalid
byte m_penalties; // NEW: accumulated time penalties in seconds to be added
};


Adititional data:


Track and Team IDs
ID Track
0 Melbourne
1 Sepang
2 Shanghai
...
23 Texas Short
24 Suzuka Short

Team Team ID
Mercedes 4
Redbull 0
...
Ferrari 1
Sauber 5

Classic Team Team ID
Williams 1992 0
...
Redbull 2010 11
McLaren 1991 12

Driver ID
Lewis Hamilton 9
Valtteri Bottas 15
...
Gert Waldmuller 33
Julian Quesada 34


The code:
To receive this data, I have used the following code that can be found here (https://doc.qt.io/qt-5/qtnetwork-broadcastreceiver-example.html):


// receiver.h

#ifndef RECEIVER_H
#define RECEIVER_H
#include <QWidget>

class QLabel;
class QPushButton;
class QUdpSocket;
class QAction;

class Receiver : public QWidget
{
Q_OBJECT

public:
Receiver(QWidget *parent = 0);

private slots:
void processPendingDatagrams();

private:
QLabel *statusLabel;
QPushButton *quitButton;
QUdpSocket *udpSocket;
};

#endif




// receiver.cpp

#include <QtWidgets>
#include <QtNetwork>
#include "receiver.h"

Receiver::Receiver(QWidget *parent)
: QWidget(parent)
{
statusLabel = new QLabel(tr("Listening for broadcasted messages"));
statusLabel->setWordWrap(true);

quitButton = new QPushButton(tr("&Quit"));

udpSocket = new QUdpSocket(this);
udpSocket->bind(45454, QUdpSocket::ShareAddress);

connect(udpSocket, SIGNAL(readyRead()),
this, SLOT(processPendingDatagrams()));
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));

QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch(1);
buttonLayout->addWidget(quitButton);
buttonLayout->addStretch(1);

QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(statusLabel);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);

setWindowTitle(tr("Broadcast Receiver"));
}

void Receiver::processPendingDatagrams()
{
while (udpSocket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
udpSocket->readDatagram(datagram.data(), datagram.size());
statusLabel->setText(tr("Received datagram: \"%1\"")
.arg(datagram.data()));
}
}


The current situation:
I was successfully able to connect to the UDP telemetry but the data was just not readable. I could tell that it was the correct data because different kinds of data show up for different things I did in the game. However, it is still unreadable.
Here is a video and a snapshot of how the output looked like:

12709

https://www.youtube.com/watch?v=UkACYG8HV9Q

Processing the data:
I assume that this reading issue has to do with how the data was processed and I can see a couple of problems in my early attempt at capturing it.
The first is casting. I assume that the data can't just be directly read as a series of integer values. I have to create a similar structure to the one that they were sent out as. However, looking at the data structure, I see two different structs, struct UDPPacket and struct CarUDPDat. How can I tell which is which when I am casting? It is hard to believe that the data for CarUDPData will find it's way automatically into it's place in UDPPacket, or can it? How do I go about executing such a casting method in my code?
The second is endiness. If I understood it correctly, the data is being sent in little endian and I have to make changes to my reading function so that it adheres to that order if it isn't already so.

Providing the data to the interface:
If I were to create those two structs, and have their values made available to the qml side of the app in order to render different graphics with them, I assume it would be best to have a model class to pass along the data and to keep the view updated. However, there will always be only one instance of each struct at a time which makes me wonder if an object oriented approach is a proper one here. That is, a class created for each of the two structs which holds all the necessary values.
If I were to skip the object oriented approach to the UDP structures, maybe I can have them as private members of the receiver class, or a separate class, and create public member functions that return values of specific variables that are requested.

The interactive overlay:
A last thing I want to mention is that if possible, I would want make this overlay controlled by viewers watching the stream by allowing them to change between different telemetry layouts. This can be done through installing a Twitch stream extension on my channel that transmitts user clicks/picks on the video player to my overlay app. This in turn changes which view is the active one.
The app is added to OBS, which is used for streaming the game, as a window capture with an added effect of removing a specified color and replacing it with transparency where the underlying capture, the F1 game capture, is displayed.
12711

More about Twitch extensions: Twitch TV Extensions (https://www.twitch.tv/p/extensions)
An example of such an app/overlay: Gamepadviewer.com (https://gamepadviewer.com/)

I would appreciate some advice on the approach to choose here as well as how to handle the reading/casting of the UDP telemetry data to make usable and readable for my application.
I hope that I am making some sense here with my descriptions. All help, tips and advice is very appreciated. Thanks. :)

Nizars
30th November 2017, 20:26
Update:

I was able to read the data after casting it using this method:



UDPPacket* updPacket = reinterpret_cast<UDPPacket*>(datagram.data());


and then outputting it to the console using:



qDebug() << "Time: " << updPacket->m_time;
qDebug() << "Lap time: " << updPacket->m_lapTime;
qDebug() << "Lap distance: " << updPacket->m_lapDistance;
qDebug() << "Total distance: " << updPacket->m_totalDistance;
qDebug() << "World space position X: " << updPacket->m_x;
qDebug() << "World space position Y: " << updPacket->m_y;
qDebug() << "World space position Z: " << updPacket->m_z;
...


Below is a sample of the output I got while driving:



Time: 247.21
Lap time: 3.21761
Lap distance: 256.501
Total distance: 5557.78
World space position X: -297.622
World space position Y: 1.87141
World space position Z: 287.999
Speed: 71.6476
World space velocity X: -51.4009
World space velocity Y: 0.235372
World space velocity Y: -49.9126
World space right direction X: 0.696872
World space right direction Y: 0.000541129
World space right direction Z: -0.717195
World space forward direction X: -0.717193
World space forward direction Y: 0.00341658
World space forward direction Z: -0.696867
Susp. position RL: 17.8514
Susp. position RR: 17.1895
Susp. position FL: 13.5203
Susp. position FR: 13.2659
Susp. velocity RL: -10.5386
Susp. velocity RR: -3.86364
Susp. velocity FL: -18.3728
Susp. velocity FR: -4.74988
Susp. speed RL: 71.6071
Susp. speed RR: 71.6319
Susp. speed FL: 71.7294
Susp. speed FR: 71.7293
Throttle: 0
Steer: 0
Brake: 0
Clutch: 0
Gear: 8
G. force latitude: -0.0710501
G. force longitude: -0.792866
Lap: 1
Engine rate: 10318.3
SLI Pro native support: 0
Car position: 1
Kers level: 400000
Kers max level: 400000
DRS: 0
Traction control: 0.5
Anti-lock brakes: 1
Fuel in tank: 10
Fuel capacity: 105
In pits: 0
Sector: 0
Sector 1 time: 0
Sector 2 time: 0
Brakes temperature RL: 19.8746
Brakes temperature RR: 19.8746
Brakes temperature FL: 19.8746
Brakes temperature FR: 19.8746
Tyres pressure RL: 21.5
Tyres pressure RR: 21.5
Tyres pressure FL: 23
Tyres pressure FR: 23
Team ID: 4
Total laps: 1
Track size: 5301.28
Last lap time: 96.2424
Max rpm: 13600
Idle rpm: 4300
Max gears: 9
Session type: 0


It seems to match what I was doing in the game.
Now, I will try to find a way to handle those structs and make them available to the qml view.

Nizars
1st December 2017, 20:37
Any ideas on how to deal with the captured data?

I am thinking of going with the following approach:

Two C++ classes, Telemetry and TelemetryData. Telemetry has a TelemetryData private member instance. Telemetry captures the UDP data and sends it to TelemetryData by calling a function of TelemetryData setUDPPacket(Type udp packet data). This function would take either a QByteArray of the UDP data and then cast it internally to the packet struct to make it readable, or it will take a pointer to a struct that it has already been cast as.

In other words:
Option A:
Telemetry class captures data.
Calls function of it's private instance of TelemetryData.
The function setUDPPacket(QByteArray udpPacketData) takes a QByteArray and casts it as a struct to an instance of said struct to make the data available.

Option B:
Telemetry class captures data.
Calls function setUDPPacket(UDPPacket *udpPacketData) takes a pointer to a UDPPacket struct that the Telemetry class has cast the QByteArray as.
TelemetryData copies the udpPacketData to it's instance of the UDPPacket struct to make the data available.

main.cpp registers the class Telemetry and sets an instance of both Telemetry and TelemetryData as context properties. TelemetryData is registered through a pointer that is retrieved from Telemetry's function getTelemetryData() which returns a pointer of it's instance of TelemetryData.
This makes the TelemetryData class accessible by the QML side.

The TelemetryData class has get functions for all the variables in the struct UDPPacket such as getSpeed(), getLap() and so on. Those functions are called in the QML side to display the current UDP data.
To make this possible, the functions are made available through the Q_PROPERTY macro or the Q_GADGET macro.

The last problem that I will solve is how to update the view. One way is to make variables notify on change through the Q_GADGET or Q_PROPERTY macro as in Q_PROPERTY(float lapTime READ getLapTime NOTIFY lapTimeChanged).

The problem here is that the struct UDPPacket data is replaced with new data at once. This means that I have to create xChanged() for all the struct variable and call all of them every time the struct data is replaced when a new UDPPacket arrives.

A better method in my opinion would be to skip the NOTIFY method and all the xChanged signals and just make the QML side refreshes all it's data at a rate of sixty times every second, that is because the UDP data is sent at 60 Hz.

Any thoughts on this approach? And any tips on making it as lightweight as possible?

d_stranz
1st December 2017, 22:36
UDP data is sent at 60 Hz.

Any thoughts on this approach? And any tips on making it as lightweight as possible?

If what you are doing with this data is mostly displaying it to the user, then I would throw most of it away. A UI that changes at 60 Hz is ridiculous for displaying to a user. 1 - 2 Hz is more reasonable. You could average values at 60 Hz, but update the display with the average no more than once or twice a second. Humans can't comprehend and act on anything at a faster rate than that.

One thing you should be careful about - if the data being sent is newly allocated memory for each call, then be sure you properly delete it when you are done, otherwise your program will quickly come to a grinding halt as it runs out of resources.

Nizars
25th January 2018, 01:57
Thanks for the help, I have now achieved the goal. I can update this post later with my source code for those looking for a similar solution.