Updated. /JL

This commit is contained in:
2025-04-16 20:51:53 +02:00
parent 392431789b
commit 63df3e9ffb
15 changed files with 1309 additions and 0 deletions

78
dualsense.py Normal file
View File

@@ -0,0 +1,78 @@
import pygame
from pydualsense import *
BATTERY_STATE = {
"0": "Discharging",
"1": "Charging",
"2": "Full",
"11": "Not charging",
"15": "Error",
"10": "Temp or voltage out of range"
}
# Initialize pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("DualSense Demo with Pygame")
font = pygame.font.SysFont(None, 48)
clock = pygame.time.Clock()
# Initialize DualSense
ds = pydualsense()
ds.init()
# Set initial LED color
ds.light.setColorI(0, 100, 255)
# Configure adaptive trigger
ds.triggerL.setMode(TriggerModes.Rigid)
ds.triggerL.setForce(1, 255)
running = True
while running:
screen.fill((30, 30, 30))
# Read inputs from DualSense
text_lines = [
f"LX: {ds.state.LX:.2f}, LY: {ds.state.LY:.2f}",
f"RX: {ds.state.RX:.2f}, RY: {ds.state.RY:.2f}",
f"L2: {ds.state.L2:.2f}, R2: {ds.state.R2:.2f}",
f"Accel: {ds.state.accelerometer.X:.2f}, {ds.state.accelerometer.Y:.2f}, {ds.state.accelerometer.Z:.2f}",
f"Gyro: {ds.state.gyro.Roll:.2f}, {ds.state.gyro.Pitch:.2f}, {ds.state.gyro.Yaw:.2f}",
f"Power: {ds.battery.Level:4}%, State: {BATTERY_STATE[str(ds.battery.State)]}"
]
for i, line in enumerate(text_lines):
text = font.render(line, True, (255, 255, 255))
screen.blit(text, (20, 40 + i * 50))
# Button-based actions
if ds.state.square:
ds.light.setColorI(255, 0, 0) # Red
elif ds.state.triangle:
ds.light.setColorI(0, 255, 0) # Green
elif ds.state.circle:
ds.light.setColorI(0, 0, 255) # Blue
elif ds.state.cross:
ds.light.setColorI(255, 255, 255) # White
#ds.setLeftMotor(200)
#ds.setRightMotor(200) # Heavy rumble
# Handle pygame events
for event in pygame.event.get():
if event.type == pygame.QUIT: # Exit on window close or PS button
running = False
if ds.state.ps:
running = False
pygame.display.flip()
clock.tick(60)
# Clean up
ds.setLeftMotor(0)
ds.setRightMotor(0)
ds.light.setColorI(0, 0, 255)
ds.close()
pygame.quit()

151
joystick.py Normal file
View File

@@ -0,0 +1,151 @@
import pygame
pygame.init()
# This is a simple class that will help us print to the screen.
# It has nothing to do with the joysticks, just outputting the
# information.
class TextPrint:
def __init__(self):
self.reset()
self.font = pygame.font.Font(None, 25)
def tprint(self, screen, text):
text_bitmap = self.font.render(text, True, (0, 0, 0))
screen.blit(text_bitmap, (self.x, self.y))
self.y += self.line_height
def reset(self):
self.x = 10
self.y = 10
self.line_height = 15
def indent(self):
self.x += 10
def unindent(self):
self.x -= 10
def main():
# Set the width and height of the screen (width, height), and name the window.
screen = pygame.display.set_mode((500, 700))
pygame.display.set_caption("Joystick example")
# Used to manage how fast the screen updates.
clock = pygame.time.Clock()
# Get ready to print.
text_print = TextPrint()
# This dict can be left as-is, since pygame will generate a
# pygame.JOYDEVICEADDED event for every joystick connected
# at the start of the program.
joysticks = {}
done = False
while not done:
# Event processing step.
# Possible joystick events: JOYAXISMOTION, JOYBALLMOTION, JOYBUTTONDOWN,
# JOYBUTTONUP, JOYHATMOTION, JOYDEVICEADDED, JOYDEVICEREMOVED
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True # Flag that we are done so we exit this loop.
if event.type == pygame.JOYBUTTONDOWN:
print("Joystick button pressed.")
if event.button == 0:
joystick = joysticks[event.instance_id]
if joystick.rumble(0.7, 1.0, 1000):
print(f"Rumble effect played on joystick {event.instance_id}")
if event.type == pygame.JOYBUTTONUP:
print("Joystick button released.")
# Handle hotplugging
if event.type == pygame.JOYDEVICEADDED:
# This event will be generated when the program starts for every
# joystick, filling up the list without needing to create them manually.
joy = pygame.joystick.Joystick(event.device_index)
joysticks[joy.get_instance_id()] = joy
print(f"Joystick {joy.get_instance_id()} connencted")
if event.type == pygame.JOYDEVICEREMOVED:
del joysticks[event.instance_id]
print(f"Joystick {event.instance_id} disconnected")
# Drawing step
# First, clear the screen to white. Don't put other drawing commands
# above this, or they will be erased with this command.
screen.fill((255, 255, 255))
text_print.reset()
# Get count of joysticks.
joystick_count = pygame.joystick.get_count()
text_print.tprint(screen, f"Number of joysticks: {joystick_count}")
text_print.indent()
# For each joystick:
for joystick in joysticks.values():
jid = joystick.get_instance_id()
text_print.tprint(screen, f"Joystick {jid}")
text_print.indent()
# Get the name from the OS for the controller/joystick.
name = joystick.get_name()
text_print.tprint(screen, f"Joystick name: {name}")
guid = joystick.get_guid()
text_print.tprint(screen, f"GUID: {guid}")
power_level = joystick.get_power_level()
text_print.tprint(screen, f"Joystick's power level: {power_level}")
# Usually axis run in pairs, up/down for one, and left/right for
# the other. Triggers count as axes.
axes = joystick.get_numaxes()
text_print.tprint(screen, f"Number of axes: {axes}")
text_print.indent()
for i in range(axes):
axis = joystick.get_axis(i)
text_print.tprint(screen, f"Axis {i} value: {axis:>6.3f}")
text_print.unindent()
buttons = joystick.get_numbuttons()
text_print.tprint(screen, f"Number of buttons: {buttons}")
text_print.indent()
for i in range(buttons):
button = joystick.get_button(i)
text_print.tprint(screen, f"Button {i:>2} value: {button}")
text_print.unindent()
hats = joystick.get_numhats()
text_print.tprint(screen, f"Number of hats: {hats}")
text_print.indent()
# Hat position. All or nothing for direction, not a float like
# get_axis(). Position is a tuple of int values (x, y).
for i in range(hats):
hat = joystick.get_hat(i)
text_print.tprint(screen, f"Hat {i} value: {str(hat)}")
text_print.unindent()
text_print.unindent()
# Go ahead and update the screen with what we've drawn.
pygame.display.flip()
# Limit to 30 frames per second.
clock.tick(30)
if __name__ == "__main__":
main()
# If you forget this line, the program will 'hang'
# on exit if running from IDLE.
pygame.quit()

49
main.py Normal file
View File

@@ -0,0 +1,49 @@
import pygame
import pygameController as PC
if __name__ == "__main__":
pygame.init()
done = False
joystick_count = pygame.joystick.get_count()
joysticks = {}
if joystick_count == 0:
print("No controllers found...Exiting!")
done = True
while not done:
# Event processing step.
# Possible joystick events: JOYAXISMOTION, JOYBALLMOTION, JOYBUTTONDOWN,
# JOYBUTTONUP, JOYHATMOTION, JOYDEVICEADDED, JOYDEVICEREMOVED
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True # Flag that we are done so we exit this loop.
if event.type == pygame.JOYBUTTONDOWN:
print("Joystick button pressed.")
if event.button == 0:
joystick = joysticks[event.instance_id]
try:
joystick.rumble(0.7, 1.0, 1000)
except AttributeError:
print("Rumble is not supported on this joystick.")
except Exception as e:
print(f"Rumble call failed: {e}")
if event.type == pygame.JOYBUTTONUP:
print("Joystick button released.")
# Handle hotplugging
if event.type == pygame.JOYDEVICEADDED:
# This event will be generated when the program starts for every
# joystick, filling up the list without needing to create them manually.
joy = pygame.joystick.Joystick(event.device_index)
joysticks[joy.get_instance_id()] = PC.controller.Controllers(joy)
if event.type == pygame.JOYDEVICEREMOVED:
del joysticks[event.instance_id]
print(f"Joystick {event.instance_id} disconnected")

View File

@@ -0,0 +1 @@
from . import controller

View File

@@ -0,0 +1,29 @@
import pygame
from pygameController.controlsbase import ControlsBase
from pygameController.dualsense_controller import DualSenseController
from pygameController.dualsense_edge_controller import DualSenseEdgeController
from pygameController.logitech_f310_controller import LogitechF310Controller
from pygameController.logitech_f510_controller import LogitechF510Controller
from pygameController.logitech_f710_controller import LogitechF710Controller
from pygameController.xbox_controller import XboxController
from pygameController.generic_controller import GenericController
from pygameController.logitech_dual_action_controller import LogitechDualActionController
CONTROLLERS = {
"DualSense Wireless Controller": DualSenseController,
"DualSense Edge Wireless Controller": DualSenseEdgeController,
"Logitech Gamepad F310": LogitechF310Controller,
"Logitech Gamepad F510": LogitechF510Controller,
"Logitech Gamepad F710": LogitechF710Controller,
"Logitech Dual Action": LogitechDualActionController,
"Xbox": XboxController
}
class Controllers:
def __init__(self, joy):
self.controllers = []
if not joy.get_name() in CONTROLLERS:
self.controllers.append(GenericController(joy))
else:
self.controllers.append(CONTROLLERS[joy.get_name()](joy))

View File

@@ -0,0 +1,33 @@
"""
This is an abstract baseclass for the controls of snake.
"""
from abc import ABC, abstractmethod
class ControlsBase(ABC):
@abstractmethod
def handle_input(self, event):
pass
@abstractmethod
def left(self):
pass
@abstractmethod
def right(self):
pass
@abstractmethod
def up(self):
pass
@abstractmethod
def down(self):
pass
@abstractmethod
def pause(self):
pass
@abstractmethod
def rumble(self):
pass

View File

@@ -0,0 +1,70 @@
import os
import time
import numpy as np
import sounddevice as sd
import alsaaudio
import pulsectl
class DualSenseAudio:
def __init__(self):
self.alsa_devices = self._get_alsa_devices()
self.pulse_devices = self._get_pulseaudio_devices()
self.dualsense_device = self._detect_dualsense()
def _get_alsa_devices(self):
try:
cards = alsaaudio.cards()
return cards
except Exception as e:
print("ALSA detection failed:", e)
return []
def _get_pulseaudio_devices(self):
try:
pulse = pulsectl.Pulse("dualsense-audio")
sinks = pulse.sink_list()
return sinks
except Exception as e:
print("PulseAudio detection failed:", e)
return []
def _detect_dualsense(self):
# Check ALSA names
for card in self.alsa_devices:
if "DualSense" in card:
return {'type': 'alsa', 'name': card}
# Check PulseAudio sinks
for sink in self.pulse_devices:
if "dualsense" in sink.description.lower():
return {'type': 'pulse', 'name': sink.name}
return None
def play_tone(self, frequency=440.0, duration=2.0, volume=0.5):
if not self.dualsense_device:
print("DualSense speaker not found.")
return
print(f"Playing tone on DualSense ({self.dualsense_device['type']})...")
fs = 48000 # Sample rate
t = np.linspace(0, duration, int(fs * duration), False)
tone = np.sin(frequency * 2 * np.pi * t) * volume
audio = tone.astype(np.float32)
if self.dualsense_device['type'] == 'pulse':
sd.play(audio, samplerate=fs, device=self.dualsense_device['name'])
elif self.dualsense_device['type'] == 'alsa':
device_index = self.alsa_devices.index(self.dualsense_device['name'])
sd.play(audio, samplerate=fs, device=device_index)
sd.wait()
def list_devices(self):
print("ALSA Devices:")
for card in self.alsa_devices:
print(f" - {card}")
print("\nPulseAudio Devices:")
for sink in self.pulse_devices:
print(f" - {sink.name} ({sink.description})")

View File

@@ -0,0 +1,74 @@
from pygameController.controlsbase import ControlsBase
from pydualsense import *
BATTERY_STATE = {
"0": "Discharging",
"1": "Charging",
"2": "Full",
"11": "Not charging",
"15": "Error",
"10": "Temp or voltage out of range"
}
class DualSenseController(ControlsBase):
def __init__(self, joy):
self.device = pydualsense()
self.device.init()
self.name = self.device.device.get_product_string()
self.powerlevel = self.device.battery.Level
self.batterystate = BATTERY_STATE[str(self.device.battery.State)]
self.set_player_id(PlayerID.PLAYER_1)
print(f"{self.name} connected")
print(f"Power level: {self.powerlevel}")
print(f"Battery state: {self.batterystate}")
def handle_input(self, event):
pass
def set_led(self, red: int, green: int, blue: int):
self.device.light.setColorI(red, green, blue)
def set_player_id(self, playerid: PlayerID):
self.device.light.setPlayerID(playerid)
def left(self):
pass
def right(self):
pass
def up(self):
pass
def down(self):
pass
def pause(self):
pass
def rumble(self):
pass
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
def powerlevel(self) -> str:
return self._powerlevel
@powerlevel.setter
def powerlevel(self, lvl: str) -> None:
self._powerlevel = lvl
@property
def batterystate(self) -> int:
return self._batterystate
@batterystate.setter
def batterystate(self, state) -> None:
self._batterystate = state

View File

@@ -0,0 +1,199 @@
import time
import threading
import numpy as np
import sounddevice as sd
import alsaaudio
import pulsectl
from pydualsense import *
class DualSenseEdgeController:
def __init__(self):
# DualSense input/output interface
self.ds = pydualsense()
self.ds.init()
self._listening = False
self._bindings = {}
# Audio detection
self.alsa_devices = self._get_alsa_devices()
self.pulse_devices = self._get_pulseaudio_devices()
self.dualsense_audio_device = self._detect_dualsense_audio()
print("DualSense initialized.")
# ---------------------- Device Controls ----------------------
def set_rumble(self, small_motor: int, big_motor: int):
self.ds.setRumble(small_motor, big_motor)
def stop_rumble(self):
self.set_rumble(0, 0)
def set_led_color(self, r: int, g: int, b: int):
self.ds.setLightBarColor(r, g, b)
def set_trigger_effects(self, left_mode='Off', right_mode='Off', force=0):
left = getattr(TriggerModes, left_mode.upper(), TriggerModes.Off)
right = getattr(TriggerModes, right_mode.upper(), TriggerModes.Off)
self.ds.triggerL.setMode(left)
self.ds.triggerR.setMode(right)
if force > 0:
self.ds.triggerL.setForce(force)
self.ds.triggerR.setForce(force)
# ---------------------- Predefined Rumble Patterns ----------------------
def rumble_pattern(self, pattern: str, duration: float = 1.0):
patterns = {
"pulse": self._pulse_rumble,
"heartbeat": self._heartbeat_rumble,
"buzz": self._buzz_rumble,
"wave": self._wave_rumble,
"alarm": self._alarm_rumble,
}
if pattern in patterns:
threading.Thread(target=patterns[pattern], args=(duration,), daemon=True).start()
else:
print(f"Unknown rumble pattern: {pattern}")
def _pulse_rumble(self, duration):
end = time.time() + duration
while time.time() < end:
self.set_rumble(50, 150)
time.sleep(0.2)
self.stop_rumble()
time.sleep(0.2)
def _heartbeat_rumble(self, duration):
end = time.time() + duration
while time.time() < end:
self.set_rumble(200, 200)
time.sleep(0.1)
self.stop_rumble()
time.sleep(0.1)
self.set_rumble(100, 100)
time.sleep(0.1)
self.stop_rumble()
time.sleep(0.4)
def _buzz_rumble(self, duration):
self.set_rumble(80, 255)
time.sleep(duration)
self.stop_rumble()
def _wave_rumble(self, duration):
start = time.time()
while time.time() - start < duration:
for i in range(0, 256, 25):
self.set_rumble(i, 255 - i)
time.sleep(0.05)
for i in reversed(range(0, 256, 25)):
self.set_rumble(i, 255 - i)
time.sleep(0.05)
self.stop_rumble()
def _alarm_rumble(self, duration):
end = time.time() + duration
while time.time() < end:
self.set_rumble(255, 0)
time.sleep(0.1)
self.set_rumble(0, 255)
time.sleep(0.1)
self.stop_rumble()
# ---------------------- Input Listener + Bindings ----------------------
def bind(self, button: str, action: callable):
"""Bind a button to a callable. Ex: controller.bind('cross', lambda: rumble_pattern('buzz'))"""
self._bindings[button] = action
def start_input_listener(self):
def listen():
while self._listening:
#self.ds.update()
for button, action in self._bindings.items():
if getattr(self.ds, button, False):
action()
self._listening = True
thread = threading.Thread(target=listen, daemon=True)
thread.start()
def stop_input_listener(self):
self._listening = False
# ---------------------- Audio Output ----------------------
def _get_alsa_devices(self):
try:
return alsaaudio.cards()
except Exception:
return []
def _get_pulseaudio_devices(self):
try:
pulse = pulsectl.Pulse("dualsense-audio")
return pulse.sink_list()
except Exception:
return []
def _detect_dualsense_audio(self):
# Check ALSA names
for card in self.alsa_devices:
if "DualSense" in card:
return {'type': 'alsa', 'name': card}
# Check PulseAudio sinks
for sink in self.pulse_devices:
if "dualsense" in sink.description.lower():
return {'type': 'pulse', 'name': sink.name}
return None
def play_tone(self, frequency=440.0, duration=2.0, volume=0.5):
if not self.dualsense_audio_device:
print("DualSense speaker not detected.")
return
print(f"Playing tone on DualSense ({self.dualsense_audio_device['type']})...")
fs = 48000 # Sample rate
t = np.linspace(0, duration, int(fs * duration), False)
tone = np.sin(frequency * 2 * np.pi * t) * volume
audio = tone.astype(np.float32)
try:
if self.dualsense_audio_device['type'] == 'pulse':
sd.play(audio, samplerate=fs, device=self.dualsense_audio_device['name'])
elif self.dualsense_audio_device['type'] == 'alsa':
device_index = self.alsa_devices.index(self.dualsense_audio_device['name'])
sd.play(audio, samplerate=fs, device=device_index)
sd.wait()
except Exception as e:
print("Failed to play tone:", e)
def list_audio_devices(self):
print("ALSA Devices:")
for card in self.alsa_devices:
print(f" - {card}")
print("\nPulseAudio Devices:")
for sink in self.pulse_devices:
print(f" - {sink.name} ({sink.description})")
# ---------------------- Cleanup ----------------------
def close(self):
self.ds.close()
if __name__ == "__main__":
controller = DualSenseController()
# Bind buttons to patterns
controller.bind("cross", lambda: controller.rumble_pattern("heartbeat", 1.5))
controller.bind("circle", lambda: controller.rumble_pattern("buzz", 0.5))
controller.bind("triangle", lambda: controller.rumble_pattern("pulse", 2))
controller.bind("square", lambda: controller.set_led_color(255, 0, 0))
# Start listening
controller.start_input_listener()

View File

@@ -0,0 +1,50 @@
import pygame
from pygameController.controlsbase import ControlsBase
class GenericController(ControlsBase):
def __init__(self, joy):
self.device = joy
self.instance_id: int = self.device.get_instance_id()
self.name = self.device.get_name()
self.guid = self.device.get_guid()
self.numaxis: int = self.device.get_numaxes()
self.axis: list = [self.device.get_axis(a) for a in range(self.numaxis)]
self.numhats: int = self.device.get_numhats()
self.hats: list = [self.device.get_hat(h) for h in range(self.numhats)]
self.numbuttons: int = self.device.get_numbuttons()
self.buttons: list = [self.device.get_button(b) for b in range(self.numbuttons)]
print(f"{self.name} connected")
print("GUID:", self.guid)
print("Axis:", self.numaxis, self.axis)
print("Hats:", self.numhats, self.hats)
print("Buttons:", self.numbuttons, self.buttons)
def handle_input(self, event):
pass
def left(self):
pass
def right(self):
pass
def up(self):
pass
def down(self):
pass
def pause(self):
pass
def rumble(self):
pass
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name

View File

@@ -0,0 +1,80 @@
"""
Logitech F310 Controller class.
This controller is a usb controller, with the following features.
(XInput mode)
6 axis
11 buttons
1 hat
(DirectInput mode)
4 axis
12 buttons
1 hat
"""
import pygame
from pygameController.controlsbase import ControlsBase
from enum import Enum
class InputMode(Enum):
DirectInput = 1
XInput = 2
class LogitechDualActionController(ControlsBase):
def __init__(self, joy):
self.device = joy
self.instance_id: int = self.device.get_instance_id()
self.name = self.device.get_name()
self.guid = self.device.get_guid()
self.powerlevel = self.device.get_power_level()
self.numaxis: int = self.device.get_numaxes()
self.axis: list = [self.device.get_axis(a) for a in range(self.numaxis)]
self.numhats: int = self.device.get_numhats()
self.hats: list = [self.device.get_hat(h) for h in range(self.numhats)]
self.numbuttons: int = self.device.get_numbuttons()
self.buttons: list = [self.device.get_button(b) for b in range(self.numbuttons)]
self.input_mode = InputMode.DirectInput
print(f"{self.name} connected")
print("GUID:", self.guid)
print("Axis:", self.numaxis, self.axis)
print("Hats:", self.numhats, self.hats)
print("Buttons:", self.numbuttons, self.buttons)
print("Input mode:", self.input_mode)
def handle_input(self, event):
pass
def left(self):
pass
def right(self):
pass
def up(self):
pass
def down(self):
pass
def pause(self):
pass
def rumble(self):
pass
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
def input_mode(self) -> int:
return self._inputmode
@input_mode.setter
def input_mode(self, mode: int) -> None:
self._inputmode = mode

View File

@@ -0,0 +1,80 @@
"""
Logitech F310 Controller class.
This controller is a usb controller, with the following features.
(XInput mode)
6 axis
11 buttons
1 hat
(DirectInput mode)
4 axis
12 buttons
1 hat
"""
import pygame
from pygameController.controlsbase import ControlsBase
from enum import Enum
class InputMode(Enum):
DirectInput = 1
XInput = 2
class LogitechF310Controller(ControlsBase):
def __init__(self, joy):
self.device = joy
self.instance_id: int = self.device.get_instance_id()
self.name = self.device.get_name()
self.guid = self.device.get_guid()
self.powerlevel = self.device.get_power_level()
self.numaxis: int = self.device.get_numaxes()
self.axis: list = [self.device.get_axis(a) for a in range(self.numaxis)]
self.numhats: int = self.device.get_numhats()
self.hats: list = [self.device.get_hat(h) for h in range(self.numhats)]
self.numbuttons: int = self.device.get_numbuttons()
self.buttons: list = [self.device.get_button(b) for b in range(self.numbuttons)]
self.input_mode = InputMode.XInput
print(f"{self.name} connected")
print("GUID:", self.guid)
print("Axis:", self.numaxis, self.axis)
print("Hats:", self.numhats, self.hats)
print("Buttons:", self.numbuttons, self.buttons)
print("Input mode:", self.input_mode)
def handle_input(self, event):
pass
def left(self):
pass
def right(self):
pass
def up(self):
pass
def down(self):
pass
def pause(self):
pass
def rumble(self):
pass
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
def input_mode(self) -> int:
return self._inputmode
@input_mode.setter
def input_mode(self, mode: int) -> None:
self._inputmode = mode

View File

@@ -0,0 +1,108 @@
"""
Logitech F310 Controller class.
This controller is a usb controller, with the following features.
(XInput mode)
6 axis
11 buttons
1 hat
(DirectInput mode)
4 axis
12 buttons
1 hat
"""
import pygame
from pygameController.controlsbase import ControlsBase
from enum import Enum
class InputMode(Enum):
DirectInput = 1
XInput = 2
class ConnectionType(Enum):
WIRED = 1
WIRELESS = 2
class LogitechF510Controller(ControlsBase):
def __init__(self, joy):
self.device = joy
self.instance_id: int = self.device.get_instance_id()
self.name = self.device.get_name()
self.numaxis: int = self.device.get_numaxis()
self.axis: list = []
self.numhats: int = self.device.get_numhats()
self.hats: list = []
self.numbuttons: int = self.device.get_numbuttons()
self.buttons: list = []
self.input_mode: InputMode.DirectInput
self.input_connection: ConnectionType.WIRED
def handle_input(self, event):
pass
def left(self):
pass
def right(self):
pass
def up(self):
pass
def down(self):
pass
def pause(self):
pass
def rumble(self):
pass
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
def axis(self) -> list:
return self._axis
@axis.setter
def axis(self) -> None:
self._axis = [self.device.get_axis(a) for a in range(self.numaxis)]
@property
def hats(self) -> list:
return self._hats
@hats.setter
def hats(self) -> None:
self.hats = [self.device.get_hats(h) for h in range(self.numhats)]
@property
def buttons(self) -> list:
return self._buttons
@buttons.setter
def buttons(self) -> None:
self._buttons = [self.device.get_buttons(b) for b in range(self.numbuttons)]
@property
def input_mode(self) -> int:
return self._inputmode
@input_mode.setter
def input_mode(self, mode: int) -> None:
self._inputmode = mode
@property
def input_connection(self) -> int:
return self._input_connection
@input_connection.setter
def input_connection(self, conn: int) -> None:
self._input_connection = conn

View File

@@ -0,0 +1,108 @@
"""
Logitech F310 Controller class.
This controller is a usb controller, with the following features.
(XInput mode)
6 axis
11 buttons
1 hat
(DirectInput mode)
4 axis
12 buttons
1 hat
"""
import pygame
from pygameController.controlsbase import ControlsBase
from enum import Enum
class InputMode(Enum):
DirectInput = 1
XInput = 2
class ConnectionType(Enum):
WIRED = 1
WIRELESS = 2
class LogitechF710Controller(ControlsBase):
def __init__(self, joy):
self.device = joy
self.instance_id: int = self.device.get_instance_id()
self.name = self.device.get_name()
self.numaxis: int = self.device.get_numaxis()
self.axis: list = []
self.numhats: int = self.device.get_numhats()
self.hats: list = []
self.numbuttons: int = self.device.get_numbuttons()
self.buttons: list = []
self.input_mode: InputMode.DirectInput
self.input_connection: ConnectionType.WIRED
def handle_input(self, event):
pass
def left(self):
pass
def right(self):
pass
def up(self):
pass
def down(self):
pass
def pause(self):
pass
def rumble(self):
pass
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
def axis(self) -> list:
return self._axis
@axis.setter
def axis(self) -> None:
self._axis = [self.device.get_axis(a) for a in range(self.numaxis)]
@property
def hats(self) -> list:
return self._hats
@hats.setter
def hats(self) -> None:
self.hats = [self.device.get_hats(h) for h in range(self.numhats)]
@property
def buttons(self) -> list:
return self._buttons
@buttons.setter
def buttons(self) -> None:
self._buttons = [self.device.get_buttons(b) for b in range(self.numbuttons)]
@property
def input_mode(self) -> int:
return self._inputmode
@input_mode.setter
def input_mode(self, mode: int) -> None:
self._inputmode = mode
@property
def input_connection(self) -> int:
return self._input_connection
@input_connection.setter
def input_connection(self, conn: int) -> None:
self._input_connection = conn

View File

@@ -0,0 +1,199 @@
import time
import threading
import numpy as np
import sounddevice as sd
import alsaaudio
import pulsectl
from pydualsense import *
class XboxController:
def __init__(self):
# DualSense input/output interface
self.ds = pydualsense()
self.ds.init()
self._listening = False
self._bindings = {}
# Audio detection
self.alsa_devices = self._get_alsa_devices()
self.pulse_devices = self._get_pulseaudio_devices()
self.dualsense_audio_device = self._detect_dualsense_audio()
print("DualSense initialized.")
# ---------------------- Device Controls ----------------------
def set_rumble(self, small_motor: int, big_motor: int):
self.ds.setRumble(small_motor, big_motor)
def stop_rumble(self):
self.set_rumble(0, 0)
def set_led_color(self, r: int, g: int, b: int):
self.ds.setLightBarColor(r, g, b)
def set_trigger_effects(self, left_mode='Off', right_mode='Off', force=0):
left = getattr(TriggerModes, left_mode.upper(), TriggerModes.Off)
right = getattr(TriggerModes, right_mode.upper(), TriggerModes.Off)
self.ds.triggerL.setMode(left)
self.ds.triggerR.setMode(right)
if force > 0:
self.ds.triggerL.setForce(force)
self.ds.triggerR.setForce(force)
# ---------------------- Predefined Rumble Patterns ----------------------
def rumble_pattern(self, pattern: str, duration: float = 1.0):
patterns = {
"pulse": self._pulse_rumble,
"heartbeat": self._heartbeat_rumble,
"buzz": self._buzz_rumble,
"wave": self._wave_rumble,
"alarm": self._alarm_rumble,
}
if pattern in patterns:
threading.Thread(target=patterns[pattern], args=(duration,), daemon=True).start()
else:
print(f"Unknown rumble pattern: {pattern}")
def _pulse_rumble(self, duration):
end = time.time() + duration
while time.time() < end:
self.set_rumble(50, 150)
time.sleep(0.2)
self.stop_rumble()
time.sleep(0.2)
def _heartbeat_rumble(self, duration):
end = time.time() + duration
while time.time() < end:
self.set_rumble(200, 200)
time.sleep(0.1)
self.stop_rumble()
time.sleep(0.1)
self.set_rumble(100, 100)
time.sleep(0.1)
self.stop_rumble()
time.sleep(0.4)
def _buzz_rumble(self, duration):
self.set_rumble(80, 255)
time.sleep(duration)
self.stop_rumble()
def _wave_rumble(self, duration):
start = time.time()
while time.time() - start < duration:
for i in range(0, 256, 25):
self.set_rumble(i, 255 - i)
time.sleep(0.05)
for i in reversed(range(0, 256, 25)):
self.set_rumble(i, 255 - i)
time.sleep(0.05)
self.stop_rumble()
def _alarm_rumble(self, duration):
end = time.time() + duration
while time.time() < end:
self.set_rumble(255, 0)
time.sleep(0.1)
self.set_rumble(0, 255)
time.sleep(0.1)
self.stop_rumble()
# ---------------------- Input Listener + Bindings ----------------------
def bind(self, button: str, action: callable):
"""Bind a button to a callable. Ex: controller.bind('cross', lambda: rumble_pattern('buzz'))"""
self._bindings[button] = action
def start_input_listener(self):
def listen():
while self._listening:
#self.ds.update()
for button, action in self._bindings.items():
if getattr(self.ds, button, False):
action()
self._listening = True
thread = threading.Thread(target=listen, daemon=True)
thread.start()
def stop_input_listener(self):
self._listening = False
# ---------------------- Audio Output ----------------------
def _get_alsa_devices(self):
try:
return alsaaudio.cards()
except Exception:
return []
def _get_pulseaudio_devices(self):
try:
pulse = pulsectl.Pulse("dualsense-audio")
return pulse.sink_list()
except Exception:
return []
def _detect_dualsense_audio(self):
# Check ALSA names
for card in self.alsa_devices:
if "DualSense" in card:
return {'type': 'alsa', 'name': card}
# Check PulseAudio sinks
for sink in self.pulse_devices:
if "dualsense" in sink.description.lower():
return {'type': 'pulse', 'name': sink.name}
return None
def play_tone(self, frequency=440.0, duration=2.0, volume=0.5):
if not self.dualsense_audio_device:
print("DualSense speaker not detected.")
return
print(f"Playing tone on DualSense ({self.dualsense_audio_device['type']})...")
fs = 48000 # Sample rate
t = np.linspace(0, duration, int(fs * duration), False)
tone = np.sin(frequency * 2 * np.pi * t) * volume
audio = tone.astype(np.float32)
try:
if self.dualsense_audio_device['type'] == 'pulse':
sd.play(audio, samplerate=fs, device=self.dualsense_audio_device['name'])
elif self.dualsense_audio_device['type'] == 'alsa':
device_index = self.alsa_devices.index(self.dualsense_audio_device['name'])
sd.play(audio, samplerate=fs, device=device_index)
sd.wait()
except Exception as e:
print("Failed to play tone:", e)
def list_audio_devices(self):
print("ALSA Devices:")
for card in self.alsa_devices:
print(f" - {card}")
print("\nPulseAudio Devices:")
for sink in self.pulse_devices:
print(f" - {sink.name} ({sink.description})")
# ---------------------- Cleanup ----------------------
def close(self):
self.ds.close()
if __name__ == "__main__":
controller = DualSenseController()
# Bind buttons to patterns
controller.bind("cross", lambda: controller.rumble_pattern("heartbeat", 1.5))
controller.bind("circle", lambda: controller.rumble_pattern("buzz", 0.5))
controller.bind("triangle", lambda: controller.rumble_pattern("pulse", 2))
controller.bind("square", lambda: controller.set_led_color(255, 0, 0))
# Start listening
controller.start_input_listener()