Reverse-Engineering the Game-Trak protocol

The Game-Trak by In2Games is a funny little PS2 or PC input device. It determines the 3D-position of two tracked objects via quite the clever method. if you remember vectors from maths-class, you probably remember that a vector going from (0,0,0) to a point in space can be defined by the 3D-Coordinates of that point. You might also remember that a vector has a certian length, and that its angles to any axis of the coordinate system can be calculated.
The Game-Trak does this in reverse. A string can be pulled out of the device and a ball-joint at the base out of which the string is pulledm tilts in the direction of the string. The length of the string, aswell as the two angles of the rotational axis of the ball-joint are measured, becoming the length of the vector, aswell as the ball-joint's angles becoming the vectors angles to the x- and z-Axis.
Those values are transmitted to the PS2 or PC which takes the angles and length to create a vector pointing in 3D-Space to the position of the tracked object, for example a hand.

Protocol

The Game-Trak connects via USB and is recognized as an HID-Device. Using libraries like hidapi in C or hid (which uses hidapi) in python, you can read some data from the Game-Trak.
The Game-Trak returns 16 bytes of data every update:

In Python
(left_hor, left_ver, left_len, right_hor, right_ver, right_len, buttons) = struct.unpack("<HHHHHHBxxx",b)
       
or C
struct game_trak_data_t {
  uint16_t left_angle_horizontal;
  uint16_t left_angle_vertical;
  uint16_t left_length;
  uint16_t right_angle_horizontal;
  uint16_t right_angle_vertical;
  uint16_t right_length;
  uint8_t  buttons; //usually 00,  10 when foot-pedal pressed
  /* 3 bytes unknown*/
};
       

The angle- and length-values roughly go from 0 to nearly 4096. With the string fully retracted, length returns some value near 4096, fully extended 0. Below is an example on how to interpret the data in python.

import struct, math
import hid
from binascii import hexlify

max_adc_angle = 4000
max_angle = 45

max_adc_len=4075 # adc reading with cables fully retracted
max_len = 3.2 #cable length when reading 0
min_len = 0.1 # how many meters out from swivel point does length get detected

def deg2rad(deg):
    return (deg/360) * 2*math.pi

def calc_3d(hor,ver,dist): #trigonometry time
    max_angle_rad = deg2rad(max_angle)
    hor_rad = (2*(hor / max_adc_angle)-1) * max_angle_rad
    ver_rad = (2*(ver / max_adc_angle)-1) * max_angle_rad
    dist_m = ((max_adc_len-dist) / max_adc_len) * (max_len - min_len)
    dist_m += min_len
    print("Hor=%.3frad Ver=%.3frad Len=%04.2fm"%(hor_rad,ver_rad,dist_m))
    
    x = dist_m * math.sin(hor_rad)
    z = dist_m * math.sin(ver_rad)
    y = dist_m #this is wrong i think but works better then the "proper" way???
    print("X%06.3f Y%06.3f Z%06.3f" % (x,y,z))
    return (x,y,z)

def parse_game_trak(b):
    (left_hor, left_ver, left_len, right_hor, right_ver, right_len, buttons) = struct.unpack("<HHHHHHBxxx",b)
    
    print(left_hor, left_ver, left_len, right_hor, right_ver, right_len)
    
    l_pos = calc_3d(left_hor, left_ver, left_len)
    r_pos = calc_3d(right_hor, right_ver, right_len)

with hid.Device(0x0982, 0x0982) as dev:
    print(f'Device manufacturer: {dev.manufacturer}')
    print(f'Product: {dev.product}')
    print(f'Serial Number: {dev.serial}')
    print("starting")

    while True:
        d=dev.read(16, 1000)
        print(hexlify(d, ' '))
        parse_game_trak(d)