GT7 is compatible with motion rig ?

  • Thread starter poumpoum
  • 686 comments
  • 188,093 views
Sorry I didnt spot this post/code example before ! I have just tried it, and it looks brilliant ! I hope to finish my code by tomorrow, aimed at XSim
Thanks again @tarnheld for your time and explanations !
 
I've got to say thank you guys. Especially Nenkai and Bornhall 👍
This is my very first Python project and i'll continue adding more things: Team Radio for example and the option for setting pit strategies before a race.

From what i saw in syncing the graphs for actual and choosen "best" lap, calculations could be made with 0,033333 seconds timedelta (1/30 on PS4 Pro)😬
But i'm not an expert in Algebra and G-forces 😅

 
In the past days I have updated gt7dashboard with some new features:
  • Time Diff Graph between Last Lap and Reference Lap
    • Under dashed line is better and over dashed line is worse than the selected reference lap
  • Picker for Reference Lap
    • Default is best lap
  • Relative Fuel Map for choosing the right Fuel Map setting in order to reach distance, remaining time and desired lap times
The Time Diff Graph is the biggest change, I had to calculate the distance and time at every point in the lap. After that I created a data set of distances and the two time columns for last lap and reference lap. The missing data for distances and times in all columns is interpolated. All this taught me a thing or two about data science and modern data science tools. So win win.
I think that the outcome is pretty good and for my time trials was accurate and helped me to spot the turns I can improve on. Since I am on controller I think it is a fair advantage :D.

Have a look at a recent screenshot:

screenshot.png
 
Now we have local_velocity, and acceleration, I'm trying to figure out roll/pitch/yaw which I thought we had natively but I was wrong....
I guess the answer is somewhere here https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles with the EulerAngles ToEulerAngles(Quaternion q) but my python code is not behaving as expected. Tried with Qc insteaf of Q, swapped Z/Y axis.... still no relevant values.

Bad code:

sinr_cosp = 2*(Qc[3]*Qc[0]+Qc[1]*Qc[2])
cosr_cosp = 1-2*(Qc[0]*Qc[0]+Qc[1]*Qc[1])
roll = math.atan2(sinr_cosp,cosr_cosp)

sinp = 2*(Qc[3]*Qc[1]-Qc[2]*Qc[0])
if abs(sinp) >= 1:
pitch = math.copysign(np.pi/2 , sinp)
else:
pitch = math.asin(sinp)

siny_cosp = 2*(Qc[3]*Qc[2]+Qc[0]*Qc[1])
cosy_cosp = 1-2*(Qc[1]*Qc[1]+Qc[2]*Qc[2])
yaw = math.atan2(siny_cosp,cosy_cosp)



Once this last "detail" will be fixed, I'll do some code clean-up and publish it and probably write a short story on how all this was possible !
 
Last edited:
Now we have local_velocity, and acceleration, I'm trying to figure out roll/pitch/yaw which I thought we had natively but I was wrong....
I guess the answer is somewhere here https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles with the EulerAngles ToEulerAngles(Quaternion q) but my python code is not behaving as expected. Tried with Qc insteaf of Q, swapped Z/Y axis.... still no relevant values.

Bad code:

sinr_cosp = 2*(Qc[3]*Qc[0]+Qc[1]*Qc[2])
cosr_cosp = 1-2*(Qc[0]*Qc[0]+Qc[1]*Qc[1])
roll = math.atan2(sinr_cosp,cosr_cosp)

sinp = 2*(Qc[3]*Qc[1]-Qc[2]*Qc[0])
if abs(sinp) >= 1:
pitch = math.copysign(np.pi/2 , sinp)
else:
pitch = math.asin(sinp)

siny_cosp = 2*(Qc[3]*Qc[2]+Qc[0]*Qc[1])
cosy_cosp = 1-2*(Qc[1]*Qc[1]+Qc[2]*Qc[2])
yaw = math.atan2(siny_cosp,cosy_cosp)



Once this last "detail" will be fixed, I'll do some code clean-up and publish it and probably write a short story on how all this was possible !
it feels okay’ish in my implementation in C for Quaternion ToEulerAngles (need more testing) but your code doesn’t look good indeed. try writing a function and feeding it each element at a time because you will need to call it with -X, -Z, -Y and W (i.e. Qc[0], Qc[2], Qc[1], Qc[3])
 
I'm trying to figure out roll/pitch/yaw
You are in for a wild ride. There are many way of describing quaternions and there are even more ways of describing euler angles. All of those will mess up your code if not accounted for.

First off, the code you refered to uses another quaternion convention than that in the PD telemetry. They have Q[0] as the scalar part, and Q[1-3] for the vector part. PD uses Q[0-2] for vector part and Q[3] for scalar part. So you will have shuffle the coordinates of the rotation quaternion or change the coordinates in you code. But wait, there is more...

Euler angles describe a rotation as three separate rotations around three separate axes in a certain order. These three axes and the order or the rotations are not mentioned and thought of as being known before. You will get only the three angles of rotation. But you will need to know the three axes and the order of rotation to make sense of the three angles. That's where a lot of confusion arises.

So your code is correct for some roll/pitch/yaw rotation triple, but not the one that PD is using. I found this paper that deals with getting any rotation triple out of a rotation quaternion. See the equations in the summary part at the end.

I fiddled around with the quaternion coefficients and found a shuffle that gets reasonable roll/pitch/yaw values for a rotation axes setup like this:
440px-RPY_angles_of_cars.png

The axes labels are not what PD is using, for PD in the world frame Y is up, X is West and Z is North, in the local frame Z is X, the vehicle direction. But the Roll/Pitch/Yaw angles match the rotations you would expect from a vehicle rotating about the axes in the picture.

Python:
import socket
import sys
import struct
import math
import datetime

# from https://github.com/oconnor663/pure_python_salsa_chacha
def mask32(x):
    return x & 0xFFFFFFFF

def add32(x, y):
    return mask32(x + y)

def left_rotate(x, n):
    return mask32(x << n) | (x >> (32 - n))

# a, b, c, and d are indexes into the 16-word block
def quarter_round(block, a, b, c, d):
    block[b] ^= left_rotate(add32(block[a], block[d]), 7)
    block[c] ^= left_rotate(add32(block[b], block[a]), 9)
    block[d] ^= left_rotate(add32(block[c], block[b]), 13)
    block[a] ^= left_rotate(add32(block[d], block[c]), 18)

def salsa20_permute(block):
    for doubleround in range(10):
        quarter_round(block, 0, 4, 8, 12)  # column 1
        quarter_round(block, 5, 9, 13, 1)  # column 2
        quarter_round(block, 10, 14, 2, 6)  # column 3
        quarter_round(block, 15, 3, 7, 11)  # column 4
        quarter_round(block, 0, 1, 2, 3)  # row 1
        quarter_round(block, 5, 6, 7, 4)  # row 2
        quarter_round(block, 10, 11, 8, 9)  # row 3
        quarter_round(block, 15, 12, 13, 14)  # row 4

def words_from_bytes(b):
    assert len(b) % 4 == 0
    return [int.from_bytes(b[4 * i : 4 * i + 4], "little") for i in range(len(b) // 4)]

def bytes_from_words(w):
    return b"".join(word.to_bytes(4, "little") for word in w)

def salsa20_block(key, nonce, blocknum):
    # This implementation doesn't support 16-byte keys.
    assert len(key) == 32
    assert len(nonce) == 8
    assert blocknum < 2 ** 64
    constant_words = words_from_bytes(b"expand 32-byte k")
    key_words = words_from_bytes(key)
    nonce_words = words_from_bytes(nonce)
    # fmt: off
    original_block = [
        constant_words[0],  key_words[0],            key_words[1],       key_words[2],
        key_words[3],       constant_words[1],       nonce_words[0],     nonce_words[1],
        mask32(blocknum),   mask32(blocknum >> 32),  constant_words[2],  key_words[4],
        key_words[5],       key_words[6],            key_words[7],       constant_words[3],
    ]
    # fmt: on
    permuted_block = list(original_block)
    salsa20_permute(permuted_block)
    for i in range(len(permuted_block)):
        permuted_block[i] = add32(permuted_block[i], original_block[i])
    return bytes_from_words(permuted_block)

def salsa20_stream(key, nonce, length):
    output = bytearray()
    blocknum = 0
    while length > 0:
        block = salsa20_block(key, nonce, blocknum)
        take = min(length, len(block))
        output.extend(block[:take])
        length -= take
        blocknum += 1
    return output

def salsa20_xor(key, nonce, message):
    stream = salsa20_stream(key, nonce, len(message))
    return bytes(x ^ y for x, y in zip(message, stream))

#https://github.com/Nenkai/PDTools/blob/master/SimulatorInterface/SimulatorInterface.cs
SendDelaySeconds = 10

ReceivePort = 33740
SendPort = 33739

port = ReceivePort

if len(sys.argv) == 2:
    # Get "IP address of Server" and also the "port number" from
    ip = sys.argv[1]
else:
    print("Run like : python3 gt7racedata.py <playstation-ip>")
    exit(1)

# Create a UDP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Bind the socket to the port
server_address = ('0.0.0.0', port)
s.bind(server_address)
s.settimeout(10)

#https://github.com/Nenkai/PDTools/blob/master/PDTools.Crypto/SimulationInterface/SimulatorInterfaceCryptorGT7.cs
def salsa20_dec(dat):
  KEY = b'Simulator Interface Packet GT7 ver 0.0'
  oiv = dat[0x40:0x44]
  iv1 = int.from_bytes(oiv, byteorder='little') # Seed IV is always located there
  iv2 = iv1 ^ 0xDEADBEAF #// Notice DEADBEAF, not DEADBEEF
  IV = bytearray()
  IV.extend(iv2.to_bytes(4, 'little'))
  IV.extend(iv1.to_bytes(4, 'little'))
  ddata = salsa20_xor(KEY[0:32], bytes(IV), dat)
  #check magic number
  magic = int.from_bytes(ddata[0:4], byteorder='little')
  if magic != 0x47375330:
    return bytearray(b'')
  return ddata

def send_hb(s):
  #send HB
  send_data = 'A'
  s.sendto(send_data.encode('utf-8'), (ip, SendPort))
  print('send heartbeat')

send_hb(s)

#see https://www.gtplanet.net/forum/threads/gt7-is-compatible-with-motion-rig.410728/post-13823560

def quat_conj(Q):
    return (-Q[0],-Q[1],-Q[2],Q[3])
def quat_vec(Q):
    return (Q[0],Q[1],Q[2])
def quat_scalar(Q):
    return Q[3]
def cross(A,B):
    return (A[1]*B[2]-A[2]*B[1],A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0])
def add(A,B):
    return (A[0]+B[0],A[1]+B[1],A[2]+B[2])
def sub(A,B):
    return (A[0]-B[0],A[1]-B[1],A[2]-B[2])
def scale(A,s):
    return (A[0]*s,A[1]*s,A[2]*s)
def quat_rot(V,Q):
    Qv = quat_vec(Q)
    U = cross(Qv,V)
    w = quat_scalar(Q)
    P = add(U,scale(V,w))
    T = scale(Qv,2)
    RR = cross(T,P)
    R = add(V,RR)
    return R

def roll_pitch_yaw(Q):
    # see http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/Quaternions.pdf
    # permute quaternion coefficients to determine
    # order of roll/pitch/yaw rotation axes, scalar comes first always
    P=(Q[3],Q[2],Q[0],Q[1])
    # e is for handedness of axes
    e = -1
    x_p = 2*(P[0]*P[2] + e*P[1]*P[3])
    pitch = math.asin(x_p)
    if math.isclose(math.fabs(pitch),math.pi/2):
        # handle singularity when pitch is +-90°
        yaw = 0
        roll = math.atan2(P[1],P[0])
    else:
        y_r = 2*(P[0]*P[1] - e*P[2]*P[3])
        x_r = 1 - 2*(P[1]*P[1] + P[2]*P[2])
        roll = math.atan2(y_r,x_r)
        y_y = 2*(P[0]*P[3] - e*P[1]*P[2])
        x_y = 1 - 2*(P[2]*P[2] + P[3]*P[3])
        yaw =math.atan2(y_y,x_y)
    return (roll,pitch,yaw)

print("Ctrl+C to exit the program")
dp_prev = 0
pknt = 0
Vp = (0,0,0)
while True:
    try:
        data, address = s.recvfrom(4096)
        pknt = pknt + 1
        print("received: %d bytes" % len(data))
        ddata = salsa20_dec(data)
        if len(ddata) > 0:
            magic = struct.unpack_from('i',ddata,0)
            P = struct.unpack_from('fff',ddata,0x4)
            print('Position',P)
            V = struct.unpack_from('fff',ddata,0x10)
            print('Global Velocity',V)
            Q = struct.unpack_from('ffff',ddata,0x1C)
            Qc = quat_conj(Q)
            Vl = quat_rot(V,Qc)
            print("Local Velocity:",Vl)
            dV = sub(Vl,Vp)
            A = scale(dV,60)
            print("Acceleration:",A)
            G = scale(A,1.0/9.81)
            print("G-Forces:",G)
            Vp = Vl
            roll,pitch,yaw = roll_pitch_yaw(Q)
            print("Roll:",roll*180/math.pi)
            print("Pitch:",pitch*180/math.pi)
            print("Yaw:",yaw*180/math.pi)

        if pknt > 900:
            send_hb(s)
            pknt = 0
    except Exception as e:
        print(e)
        send_hb(s)
        pknt = 0
        pass

I also got rid of the salsa library here and replaced it with code from salsa-pure. This should also run on Pythonista/Pyto on IOS...

Hope this helps!
 
Last edited:
@tarnheld So this are the actual G-Forces?
With that its possible to simulate the tyre degradation?
These are the g-forces resulting from car acceleration. The g-force from earth gravity is missing, but you could add it with little effort. Though i'm not sure how exactly the g-forces affect tire wear, i thought that tire wear is mainly driven by tire temperature.
 
These are the g-forces resulting from car acceleration. The g-force from earth gravity is missing, but you could add it with little effort. Though i'm not sure how exactly the g-forces affect tire wear, i thought that tire wear is mainly driven by tire temperature.

Im not sure, but the centripetal forces together with tyre temps should somehow affect the degradation i think 🤔
Cause you dont just loose tyre when they are to hot..
 
Hello everyone !
I'm very happy to say that I have released the first version of my GT7Proxy, targeted at XSim powered rigs. I have used pieces of code from many of you, so kudos to all of you !
Everything is consistent now (I just have very few spikes on forward acceleration I need to investigate, but result is great !). I'll post a video soon too.
Release is here
Code is here

1662021357970.png
 
Last edited:
they were fast to pick up the reverse engineering info and update their software. luckily there will be mentions of @Nenkai and others (not likely)
On the contrary, just got a reply from SIMRIG, and they do indeed mention Nenkai in their About-window:

Screenshot.png


Edit: To note also, that they haven't used any of Nenkai's code in their software. So the mention is for the discovery of the telemetry data.
 
Last edited:
Hello guys,
I finally found time to give it a new try.
But I don't get it running completly.
Thanks to your support I was able to get the Bokeh page running, but no data arrive.
This is the current outcome in the CMD:
Traceback (most recent call last): File "D:\Program Files\gt7dashboard-main\gt7communication.py", line 186, in run data, address = s.recvfrom(4096) TimeoutError: timed out

IP Adress for PS is ok is workeing on my Andriod device

Any idea?
 
Hello guys,
I finally found time to give it a new try.
But I don't get it running completly.
Thanks to your support I was able to get the Bokeh page running, but no data arrive.
This is the current outcome in the CMD:
Traceback (most recent call last): File "D:\Program Files\gt7dashboard-main\gt7communication.py", line 186, in run data, address = s.recvfrom(4096) TimeoutError: timed out

IP Adress for PS is ok is workeing on my Andriod device

Any idea?
That's saying you aren't receiving the data from the Playstation. You say it works fine from your phone, Do you have the phone open too? It's likely the PlayStation will only send the data to the first device that connects.
 
Hello guys,
I finally found time to give it a new try.
But I don't get it running completly.
Thanks to your support I was able to get the Bokeh page running, but no data arrive.
This is the current outcome in the CMD:
Traceback (most recent call last): File "D:\Program Files\gt7dashboard-main\gt7communication.py", line 186, in run data, address = s.recvfrom(4096) TimeoutError: timed out

IP Adress for PS is ok is workeing on my Andriod device

Any idea?
What is the command you used?
 
Ok, no phone conected. no change.
Comand in Powershell:

$Env:GT7_PLAYSTATION_IP="192.1XX.XXX.XXX"

bokeh serve .
Just a few quick thoughts here, from a Mac user (although I have this running on an old Windows laptop):

Firewall blocking access?

Tried it in the normal command line, i.e. cmd . exe?

Quotation marks around the IP, are they needed?

Edit: system didn't like me entering the cmd command as-is :D
 
Last edited:
Ok I can ping the IP address , so I assume no Firewall issue
I don't know to get the "GT7_Playstation_IP" set in CMD....sorry I'm a noob
If I skip the Quation marks, I get this error:

Code:
PS D:\Program Files\gt7dashboard-main> bokeh serve .
2022-09-22 23:01:55,243 Starting Bokeh server version 2.4.3 (running on Tornado 6.2)
2022-09-22 23:01:55,245 User authentication hooks NOT provided (default user enabled)
2022-09-22 23:01:55,250 Bokeh app running at: http://localhost:5006/gt7dashboard-main
2022-09-22 23:01:55,250 Starting Bokeh server with process id: 18280
2022-09-22 23:02:19,553 Error running application handler <bokeh.application.handlers.directory.DirectoryHandler object at 0x000002552C5569E0>: No IP set in env var GT7_PLAYSTATION_IP
File 'main.py', line 438, in <module>:
raise Exception("No IP set in env var GT7_PLAYSTATION_IP") Traceback (most recent call last):
  File "C:\Users\rdrol\AppData\Local\Programs\Python\Python310\lib\site-packages\bokeh\application\handlers\code_runner.py", line 231, in run
    exec(self._code, module.__dict__)
  File "D:\Program Files\gt7dashboard-main\main.py", line 438, in <module>
    raise Exception("No IP set in env var GT7_PLAYSTATION_IP")
Exception: No IP set in env var GT7_PLAYSTATION_IP


and this is the other output, where Iget the webpage correctly,but no data:
Code:
PS X:\xxxxxxxx\gt7dashboard-main> $Env:GT7_PLAYSTATION_IP="192.168.2.107"
PS X:\xxxxxxxx\gt7dashboard-main> bokeh serve .
2022-09-22 23:05:19,323 Starting Bokeh server version 2.4.3 (running on Tornado 6.2)
2022-09-22 23:05:19,325 User authentication hooks NOT provided (default user enabled)
2022-09-22 23:05:19,329 Bokeh app running at: http://localhost:5006/gt7dashboard-main
2022-09-22 23:05:19,329 Starting Bokeh server with process id: 17452
2022-09-22 23:05:26,797 E-1001 (BAD_COLUMN_NAME): Glyph refers to nonexistent column name. This could either be due to a misspelling or typo, or due to an expected column being missing. : key "x" value "distance", key "y" value "timedelta" [renderer: GlyphRenderer(id='1187', ...)]
2022-09-22 23:05:26,797 E-1001 (BAD_COLUMN_NAME): Glyph refers to nonexistent column name. This could either be due to a misspelling or typo, or due to an expected column being missing. : key "x" value "raceline_z", key "y" value "raceline_x" [renderer: GlyphRenderer(id='1476', ...)]
2022-09-22 23:05:26,797 E-1001 (BAD_COLUMN_NAME): Glyph refers to nonexistent column name. This could either be due to a misspelling or typo, or due to an expected column being missing. : key "x" value "raceline_z", key "y" value "raceline_x" [renderer: GlyphRenderer(id='1494', ...)]
2022-09-22 23:05:26,933 WebSocket connection opened
2022-09-22 23:05:26,934 ServerConnection created
Traceback (most recent call last):
  File "X:\xxxxxxxx\gt7dashboard-main\gt7communication.py", line 186, in run
    data, address = s.recvfrom(4096)
TimeoutError: timed out
 
Ok I can ping the IP address , so I assume no Firewall issue
I don't know to get the "GT7_Playstation_IP" set in CMD....sorry I'm a noob
If I skip the Quation marks, I get this error:
A couple of things here, off the top of my head:

Pinging doesn't really apply here, it uses something called ICMP if I recall correctly, and is not bound to a specific port on TCP/UDP. Therefore, you may still have some kind of firewall or antivirus or similar that is blocking the specific ports that the application uses to talk with the Playstation hardware. It uses port 33739 to talk TO the Playstation, and port 33740 to RECEIVE data from the Playstation. If either of those ports are blocked somewhere in or between your computer and the Playstation, it's not going to work. I assume you use wifi for your smartphone, and that it doesn't block this, seeing as it works from there.

As for setting an ENV variable in CMD exe, I believe it's simply:

set GT7_PLAYSTATION_IP=123.45.67.89 (of course replace with YOUR ip address here)

You should be able to see environment variables if you just type set in the CMD exe.

As for Powershell, I haven't used it, and it may well require the quotes to work there.

In the end, the app is basically timing out waiting for something to arrive to your computer, but it never does. And as GT7 sends data continuously even when you're not on track, my guess is that something in your computer or network is blocking that data from getting back to you (and the app).
 
Last edited:
Bornhall,
first of all: Thanks a lot for your massiv support. I realy appreciate it.

Thanks to your tips I could give it a try from CMD, but the result is the same.

Even with Firewall off, there is no connetion possible to my PS5
2022-09-23 21_32_44-Window.png
 
This is the most informative thread I've ever read. Thank you all who contributed. The SRS API definition contains wheel terrain contact type (asphalt, rumble strip or other) for each tire. Is it possible to get this data from GT7? Thanks.
 
This is the most informative thread I've ever read. Thank you all who contributed. The SRS API definition contains wheel terrain contact type (asphalt, rumble strip or other) for each tire. Is it possible to get this data from GT7? Thanks.
No, there is no such data in the telemetry from the game. Unfortunately, but for what it is this "API" was never really aimed at consumer level interaction to start with. We will in all likelihood have to make do with what it is, as it is. At least it enables motion rigs for GT Sport and GT7, which is at least something.

Haaaa, now it works!!!!
Great to hear! Never give up (never surrender) :D
 
Back