-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathGPDL.py
388 lines (334 loc) · 13.8 KB
/
GPDL.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
import time
import serial
from serial.tools import list_ports
from colorama import Fore, Back, Style
import json
import requests
import webbrowser
import platform
import numpy as np
import uuid
from tqdm import tqdm
import pygame
from pygame.locals import *
ver = "3.0.92" # Updated version
repeat = 2000
max_pause = 33
stick_treshold = 0.99 # Threshold for detecting valid axis values
# Initialize pygame
pygame.init()
# Print welcome messages
print(f" ")
print(f" ")
print(" ██████╗ ██████╗ ██████╗ " + Fore.LIGHTRED_EX + "██╗ " + Fore.RESET + "")
print(" ██╔════╝ ██╔══██╗██╔══██╗" + Fore.LIGHTRED_EX + "██║ " + Fore.RESET + Fore.LIGHTRED_EX + " " + "Gamepad Latency Tester" + Fore.RESET + " " + ver)
print(" ██║ ███╗██████╔╝██║ ██║" + Fore.LIGHTRED_EX + "██║ " + Fore.RESET + " Website: https://gamepadla.com")
print(" ██║ ██║██╔═══╝ ██║ ██║" + Fore.LIGHTRED_EX + "██║ " + Fore.RESET + " Author: John Punch")
print(" ╚██████╔╝██║ ██████╔╝" + Fore.LIGHTRED_EX + "███████╗" + Fore.RESET + "")
print(" ╚═════╝ ╚═╝ ╚═════╝ " + Fore.LIGHTRED_EX + "╚══════╝" + Fore.RESET + "")
print(f" ")
print(f" ")
# Display credits and links
print(f"Credits:")
print(f"My Reddit page: https://reddit.com/user/JohnnyPunch")
print(f"Support Me: https://ko-fi.com/gamepadla")
print(f"Guide to using GPDL: https://gamepadla.com/gpdl-tester-guide.pdl")
print("\033[33mDon't forget to update the firmware of GPDL device! https://gamepadla.com/updating-gpdl-firmware.pdl\033[0m")
print(f"---")
def connect_to_com_port(port, baud_rate=115200, max_attempts=5):
attempts = 0
while attempts < max_attempts:
try:
ser = serial.Serial(port, baud_rate)
print(f"\033[32mSuccessfully connected to {port}\033[0m")
return ser
except serial.SerialException as e:
attempts += 1
print(f"\033[31mAttempt {attempts}/{max_attempts}: Error opening {port}: {e}\033[0m")
if attempts < max_attempts:
print(f"Retrying in 5 seconds...")
time.sleep(5)
else:
print(f"\033[31mFailed to connect after {max_attempts} attempts. Exiting.\033[0m")
exit(1)
# Get a list of connected joysticks
joysticks = [pygame.joystick.Joystick(x) for x in range(pygame.joystick.get_count())]
# Check if any joystick is connected
if not joysticks:
print(" ")
print("\033[31mNo connected gamepads found!\033[0m")
input("Press Enter to exit...")
exit(1)
# List available COM ports
available_ports = [port.device for port in list_ports.comports()]
print(" ")
print("Available COM ports:")
for i, port in enumerate(available_ports):
port_name = list_ports.comports()[i].description
print(f"{i + 1} - {port_name}")
if len(available_ports) == 1:
port = available_ports[0]
print(f"\033[32mOnly one COM port found. Automatically selected: {port}\033[0m")
else:
port_num = int(input("Enter the COM port number for GPDL: ")) - 1
try:
port = available_ports[port_num]
except IndexError:
print("\033[31mInvalid COM port number. Exiting.\033[0m")
time.sleep(5)
exit()
# Connect to the specified COM port with retry logic
try:
ser = connect_to_com_port(port)
except Exception as e:
print(f"\033[31mUnexpected error: {e}\033[0m")
exit(1)
# List available gamepads
print(" ")
print("Available gamepads:")
for i, joystick in enumerate(joysticks):
print(f"{i + 1} - {joystick.get_name()}")
if len(joysticks) == 1:
joystick = joysticks[0]
joystick.init()
print(f"\033[32mOnly one gamepad found. Automatically selected: {joystick.get_name()}\033[0m")
else:
joystick_num = int(input("Enter the gamepad number: ")) - 1
try:
joystick = joysticks[joystick_num]
joystick.init()
except IndexError:
print("\033[31mInvalid gamepad number!\033[0m")
input("Press Enter to exit...")
exit(1)
# Prompt user to select test type (Button test or Stick test)
print("\n\033[1mChoose Test Type:\033[0m")
print("1 - BUTTON latency test")
print("2 - STICK W/O Resistor")
print("\033[38;5;208m⚠ WARNING: DO NOT MIX UP THE MODES! Incorrect selection may damage your gamepad. ⚠\033[0m")
print("Wait 3 seconds...")
time.sleep(3)
test_type = input("Enter test type (1/2): ")
# Set variables based on selected test type
if test_type == '1':
button_pin, down, up, method = 2, "L", "H", "ARD" # Button test
elif test_type == '2':
button_pin, down, up, method = 5, "L", "H", "STK" # Stick test (WO Resistor mode)
# Prompt user to input stick threshold
print()
try:
stick_treshold = float(input("Enter stick threshold value (default is 0.99): "))
except ValueError:
print("Invalid input. Using default threshold of 0.99.")
stick_treshold = 0.99
else:
print("\033[31mInvalid test type. Exiting.\033[0m")
ser.close()
exit(1)
if test_type in ['2', '3']:
# Choose which stick to test (left or right)
print("\n\033[1mChoose Stick to Test:\033[0m")
print("1 - Left Stick")
print("2 - Right Stick")
stick_choice = input("Enter stick number (1 or 2): ")
# Assign axes based on stick choice
if stick_choice == '1':
stick_axes_indices = [0, 1] # Left stick uses axes 0 and 1
print("Testing Left Stick")
elif stick_choice == '2':
stick_axes_indices = [2, 3] # Right stick uses axes 2 and 3
print("Testing Right Stick")
else:
print("\033[31mInvalid stick choice! Exiting.\033[0m")
exit(1)
# Initialize tracking for invalid test (positive and negative X axis detection)
positive_x_detected = False
negative_x_detected = False
# Send button_pin to Arduino
ser.write(f"{button_pin}\n".encode())
# Inform user and start the test
print("\nThe test will begin in 2 seconds...")
print("\033[33mIf the bar does not progress, try swapping the contacts.\033[0m")
time.sleep(2)
counter = 0
delays = []
prev_button_state = False
invalid_test = False # Track if the test is invalid
# Function to filter outliers from an array
def filter_outliers(array):
lower_quantile = 0.02
upper_quantile = 0.995
sorted_array = sorted(array)
lower_index = int(len(sorted_array) * lower_quantile)
upper_index = int(len(sorted_array) * upper_quantile)
return sorted_array[lower_index:upper_index + 1]
# Function to read gamepad button state
def read_gamepad_button(joystick):
for event in pygame.event.get():
if event.type == JOYBUTTONDOWN and event.joy == joystick.get_id():
return True
return False
# Function to read gamepad stick axis state
def read_gamepad_axis(joystick):
for event in pygame.event.get():
if event.type == JOYAXISMOTION and event.joy == joystick.get_id():
stick_axes = [joystick.get_axis(i) for i in stick_axes_indices]
if any(stick_treshold <= value <= 1 for value in stick_axes) or any(-1 <= value <= -stick_treshold for value in stick_axes):
return True
return False
# Function to sleep for specified milliseconds
def sleep_ms(milliseconds):
seconds = milliseconds / 1000.0
if seconds < 0:
seconds = 0
time.sleep(seconds)
# Initial button press and release for calibration
sleep_ms(2000)
ser.write(str(down).encode())
sleep_ms(100)
ser.write(str(up).encode())
sleep_ms(1000)
# Initialize progress bar for the test
with tqdm(total=repeat, ncols=76, bar_format='{l_bar}{bar} | {postfix[0]}', dynamic_ncols=False, postfix=[0]) as pbar:
while counter < repeat:
ser.write(str(down).encode())
start = time.perf_counter()
while True:
current_time = time.perf_counter()
elapsed_time = (current_time - start) * 1000
button_state = read_gamepad_button(joystick) if test_type == '1' else read_gamepad_axis(joystick)
if button_state:
end = current_time
delay = end - start
delay = round(delay * 1000, 2)
ser.write(str(up).encode())
if test_type in ['2', '3']: # Only for stick test
# Get current stick position for detection
stick_position_x = joystick.get_axis(stick_axes_indices[0]) # X axis
stick_position_y = joystick.get_axis(stick_axes_indices[1]) # Y axis
# Round the values for display
stick_position_x_rounded = round(stick_position_x, 2)
stick_position_y_rounded = round(stick_position_y, 2)
# Check if both positive and negative X detected using threshold
if stick_position_x_rounded >= stick_treshold:
positive_x_detected = True
elif stick_position_x_rounded <= -stick_treshold:
negative_x_detected = True
# If both positive and negative X detected, mark test as invalid
if positive_x_detected and negative_x_detected:
invalid_test = True
pbar.bar_format = '{l_bar}{bar} ' + Fore.RED + '| {postfix[0]}' + Fore.RESET # Change progress bar color to red
# Record delays within valid range
if delay >= 0.28 and delay < 150:
delays.append(delay)
# Update progress bar for button or stick test
if test_type == '1': # For button test, no need to show coordinates
pbar.postfix[0] = "{:05.2f} ms".format(delay)
elif test_type in ['2', '3']: # For stick test, show X and Y coordinates
pbar.postfix[0] = "{:05.2f} ms | X: {:05.2f}, Y: {:05.2f}".format(delay, stick_position_x_rounded, stick_position_y_rounded)
pbar.update(1)
counter += 1
# Adjust maximum pause time dynamically
if (delay + 16 > max_pause):
max_pause = round(delay + 33)
if max_pause > 100:
max_pause = 100
sleep = max_pause - delay
sleep_ms(sleep)
break
# Abort if no response within 400 ms
if elapsed_time > 400:
ser.write(str(up).encode())
sleep_ms(100)
break
sleep_ms(1)
# Perform statistical analysis on recorded delays
str_of_numbers = ', '.join(map(str, delays))
delay_list = filter_outliers(delays)
filteredMin = min(delay_list)
filteredMax = max(delay_list)
filteredAverage = np.mean(delay_list)
filteredAverage_rounded = round(filteredAverage, 2)
polling_rate = round(1000 / filteredAverage, 2)
jitter = np.std(delay_list)
jitter = round(jitter, 2)
# Retrieve OS information
os_name = platform.system()
uname = platform.uname()
os_version = uname.version
# Display test results
print(f" ")
print(f"\033[1mTest results:\033[0m")
print(f"------------------------------------------")
print(f"OS: {os_name} {os_version}")
print(f"Gamepad mode: {joystick.get_name()}")
print(f"Test type: {'Button' if test_type == '1' else 'Stick'}")
if test_type == '2':
print(f"Stick threshold: {stick_treshold}")
print(f" ")
print(f"Minimal latency: {filteredMin} ms")
print(f"Average latency: {filteredAverage_rounded} ms")
print(f"Maximum latency: {filteredMax} ms")
print(f" ")
print(f"Jitter: {jitter} ms")
print(f"------------------------------------------")
print(f" ")
# Check if test is invalid due to both positive and negative X detections
if invalid_test:
print(Fore.RED + "\nWarning: The test detected input on both positive and negative X axes, which indicates improper wiring. The test is not valid." + Fore.RESET)
print("\033[31mTest results cannot be submitted to the server.\033[0m")
# Prompt user to quit
input("Press Enter to exit...")
exit(1)
# Перехід на gamepadla.com (go to gamepadla.com)
answer = input('Open test results on the website (Y/N): ').lower()
if answer != 'y':
exit(1)
# Prepare data for upload to server
test_key = uuid.uuid4()
gamepad_name = input("Gamepad name: ")
connection = input("Current connection (1. Cable, 2. Bluetooth, 3. Dongle): ")
if connection == "1":
connection = "Cable"
elif connection == "2":
connection = "Bluetooth"
elif connection == "3":
connection = "Dongle"
else:
print("Invalid choice. Defaulting to Unset.")
connection = "Unset"
data = {
'test_key': str(test_key),
'version': ver,
'url': 'https://gamepadla.com',
'date': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),
'driver': joystick.get_name(),
'connection': connection,
'name': gamepad_name,
'os_name': os_name,
'sleep_time': sleep,
'os_version': os_version,
'min_latency': filteredMin,
'avg_latency': filteredAverage_rounded,
'max_latency': filteredMax,
'polling_rate': polling_rate,
'jitter': jitter,
'mathod': method,
'delay_list': str_of_numbers,
'stick_threshold': stick_treshold
}
# Send data to server and open results page
response = requests.post('https://gamepadla.com/scripts/poster.php', data=data)
if response.status_code == 200:
print("Test results successfully sent to the server.")
webbrowser.open(f'https://gamepadla.com/result/{test_key}/')
else:
print("Failed to send test results to the server.")
# Save test data locally
with open('test_data.txt', 'w') as outfile:
json.dump(data, outfile, indent=4)
# Close the serial connection
ser.close()
# Prompt user to quit
input("Press Enter to exit...")
exit(0)