Skip to content

Instantly share code, notes, and snippets.

@drscotthawley
Last active December 2, 2023 13:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drscotthawley/63369e77796f2a24cefbbe0245a467cf to your computer and use it in GitHub Desktop.
Save drscotthawley/63369e77796f2a24cefbbe0245a467cf to your computer and use it in GitHub Desktop.
PS5 DualSense Controller Listener
#! /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