Last active
December 2, 2023 13:49
-
-
Save drscotthawley/63369e77796f2a24cefbbe0245a467cf to your computer and use it in GitHub Desktop.
PS5 DualSense Controller Listener
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python | |
# OS-Agnostic PS5 DualSense Controller Listener | |
# Author: Scott H. Hawley | |
# Instructions: | |
# 1. Pair the DualSense controller with your computer | |
# 2. Install hidapi system binary, e.g. on Mac: brew install hidapi | |
# 3. Install Python packages: pip install hidapi pygame numpy | |
# 4. Run this script! | |
# working on making the mic work. will require a usb connection. | |
# No idea how to do the touchpad yet. | |
import os | |
import pygame | |
import hid | |
import numpy as np | |
pygame.font.init() | |
GAME_FONT = pygame.font.SysFont(None, 48) | |
def stickchange(stick, center=[128,128], | |
tol=5, # tolerance in "pixels" b/c sticks sometimes get stuck a bit off-center | |
): | |
for ax in [0,1]: # x ad y axes | |
if stick[ax] < center[ax]-tol or stick[ax] > center[ax]+tol: | |
return stick | |
return None | |
def get_controller_ids(): | |
vendor_id, product_id = None, None | |
for device in hid.enumerate(): | |
if "DualSense" in device['product_string']: | |
vendor_id, product_id = device['vendor_id'], device['product_id'] | |
print(f"Found: {device['product_string']} at 0x{device['vendor_id']:04x}:0x{device['product_id']:04x}") | |
break | |
return vendor_id, product_id | |
def get_controller_state(report, debug=False): | |
connection = "bluetooth" | |
if len(report) == 64: | |
connection = "usb" | |
elif len(report) != 8: | |
assert False, "Unknown connection type" | |
report = np.array(report) | |
lstick, rstick = report[1:3], report[3:5] | |
button_pad_ind = 8 if 'usb'==connection else 5 | |
dpad_state = report[button_pad_ind] & 0x0F | |
[dpadu, dpadr, dpadd, dpadl] = [dpad_state==y for y in [0,2,4,6]] | |
shapes = report[button_pad_ind] | |
[cross, circle, square, triangle] = [(shapes & (1<<y))!=0 for y in [5,6,4,7]] | |
button_id = 9 if 'usb'==connection else 6 | |
[l1,r1,l2,r2,l3,r3] = [report[button_id] & y for y in [1,2,4,8,64,128]] # 16 & 32 go where? | |
state = {'lstick':lstick, 'rstick':rstick, | |
'dpadu':dpadu, 'dpadr':dpadr, 'dpadd':dpadd, 'dpadl':dpadl, | |
'cross':cross, 'circle':circle, 'square':square, 'triangle':triangle, | |
'l1':l1, 'r1':r1, 'l2':l2, 'r2':r2, 'l3':l3,'r3':r3,} | |
if debug: | |
if lstick is not None: | |
print(f"lstick = {lstick}, ",end="",flush=True) | |
if rstick is not None: | |
print(f"rstick = {rstick}, ",end="",flush=True) | |
buttons = [dpadu, dpadr, dpadd, dpadl, cross, circle, square, triangle,l1,r1,l2,r2,l3,r3] | |
for button, label in zip(buttons, | |
["DPadU","DPadR","DpadD","DPadL","Cross","Circle","Square","Triangle","L1","R1","L2","R2","L3","R3"]): | |
if button: print(f"{label}, ",end="",flush=True) | |
return state | |
def draw_buttons_and_joysticks(window, state): | |
# Draw buttons (X, Circle, Square, Triangle) | |
img = GAME_FONT.render("L1", True, 'red' if state['l1'] else 'grey') | |
window.blit(img, (185, 60)) | |
img = GAME_FONT.render("L2", True, 'red' if state['l2'] else 'grey') | |
window.blit(img, (185, 10)) | |
img = GAME_FONT.render("R1", True, 'red' if state['r1'] else 'grey') | |
window.blit(img, (585, 60)) | |
img = GAME_FONT.render("R2", True, 'red' if state['r2'] else 'grey') | |
window.blit(img, (585, 10)) | |
img = GAME_FONT.render("Δ", True, 'red' if state['triangle'] else 'grey') | |
window.blit(img, (600, 150)) | |
#img = GAME_FONT.render("[]", True, 'red' if state['square'] else 'grey') | |
#window.blit(img, (550, 200)) | |
pygame.draw.rect(window, 'red' if state['square'] else 'grey', (550, 205, 20, 20)) | |
img = GAME_FONT.render("O", True, 'red' if state['circle'] else 'grey') | |
window.blit(img, (650, 200)) | |
img = GAME_FONT.render("X", True, 'red' if state['cross'] else 'grey') | |
window.blit(img, (600, 250)) | |
img = GAME_FONT.render("^", True, 'red' if state['dpadu'] else 'grey') | |
window.blit(img, (200, 150)) | |
img = GAME_FONT.render("<-", True, 'red' if state['dpadl'] else 'grey') | |
window.blit(img, (150, 200)) | |
img = GAME_FONT.render("->", True, 'red' if state['dpadr'] else 'grey') | |
window.blit(img, (250, 200)) | |
img = GAME_FONT.render("V", True, 'red' if state['dpadd'] else 'grey') | |
window.blit(img, (200, 250)) | |
# Draw joysticks | |
lcenter, rcenter = np.array((300,400)), np.array((500, 400)) | |
scale = 0.5 | |
lstickpos = lcenter + scale*(np.array(state['lstick']) - np.array((128,128))) | |
pygame.draw.circle(window, 'grey', lcenter, 5) # left center dot | |
pygame.draw.circle(window, 'red' if state['l3'] else 'blue', lstickpos, 20) # left active dot | |
rstickpos = rcenter + scale*(np.array(state['rstick']) - np.array((128,128))) | |
pygame.draw.circle(window, 'grey', rcenter, 5) # right center dot | |
pygame.draw.circle(window, 'red' if state['r3'] else 'blue', rstickpos, 20) # left active dot | |
def draw_controller(window): | |
pass # eh skip it for now | |
def main(): | |
vendor_id, product_id = get_controller_ids() | |
assert (vendor_id is not None) and (product_id is not None), "No DualSense controller found." | |
gamepad = hid.device() | |
gamepad.open(vendor_id, product_id) | |
gamepad.set_nonblocking(True) | |
pygame.init() | |
width, height = 800, 500 | |
window = pygame.display.set_mode((width, height)) | |
pygame.display.set_caption("PS5 Controller GUI") | |
running = True | |
default, state = None, None # save the non-active state of the controller | |
while running: | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
running = False | |
report = gamepad.read(64) | |
if report: | |
report = np.array(report) | |
state = get_controller_state(report) | |
window.fill((0, 0, 0)) # Fill the screen with black | |
draw_controller(window) # Draw the controller | |
if state: draw_buttons_and_joysticks(window, state) | |
pygame.display.flip() # Update the screen with what we've drawn. | |
pygame.quit() | |
if __name__ == "__main__": | |
main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment