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.
2017-11-30 18_28_39-F1 2017.jpg
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
};
// 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
};
To copy to clipboard, switch view to plain text mode
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
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
To copy to clipboard, switch view to plain text mode
The code:
To receive this data, I have used the following code that can be found here:
// receiver.h
#ifndef RECEIVER_H
#define RECEIVER_H
#include <QWidget>
{
Q_OBJECT
public:
private slots:
void processPendingDatagrams();
private:
};
#endif
// 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
To copy to clipboard, switch view to plain text mode
// receiver.cpp
#include <QtWidgets>
#include <QtNetwork>
#include "receiver.h"
Receiver
::Receiver(QWidget *parent
){
statusLabel
= new QLabel(tr
("Listening for broadcasted messages"));
statusLabel->setWordWrap(true);
connect(udpSocket, SIGNAL(readyRead()),
this, SLOT(processPendingDatagrams()));
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
buttonLayout->addStretch(1);
buttonLayout->addWidget(quitButton);
buttonLayout->addStretch(1);
mainLayout->addWidget(statusLabel);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
setWindowTitle(tr("Broadcast Receiver"));
}
void Receiver::processPendingDatagrams()
{
while (udpSocket->hasPendingDatagrams()) {
datagram.resize(udpSocket->pendingDatagramSize());
udpSocket->readDatagram(datagram.data(), datagram.size());
statusLabel->setText(tr("Received datagram: \"%1\"")
.arg(datagram.data()));
}
}
// 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()));
}
}
To copy to clipboard, switch view to plain text mode
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:
vlcsnap-2017-11-30-18h13m11s798.jpg
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.
2017-11-30 18_49_51-Monitor _ Restream.io.jpg
More about Twitch extensions: Twitch TV Extensions
An example of such an app/overlay: 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.
Bookmarks