From 354fa7364d14975573b5f601e842c945b3549868 Mon Sep 17 00:00:00 2001 From: Jan Lerking Date: Sun, 20 Apr 2025 09:11:54 +0200 Subject: [PATCH] Added mapping. /JL --- joystick.py | 151 ++++++++++ pygameControls/dualsense_audio.py | 70 ----- pygameControls/dualsense_controller.py | 21 ++ pygameControls/dualsense_edge_controller.py | 276 ++++++------------ pygameControls/generic_controller.py | 22 ++ .../logitech_dual_action_controller.py | 24 +- pygameControls/logitech_f310_controller.py | 22 ++ pygameControls/logitech_f510_controller.py | 23 +- pygameControls/logitech_f710_controller.py | 23 +- pygameControls/xbox_series_x_controller.py | 23 ++ 10 files changed, 388 insertions(+), 267 deletions(-) create mode 100644 joystick.py delete mode 100644 pygameControls/dualsense_audio.py diff --git a/joystick.py b/joystick.py new file mode 100644 index 0000000..7e31275 --- /dev/null +++ b/joystick.py @@ -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() \ No newline at end of file diff --git a/pygameControls/dualsense_audio.py b/pygameControls/dualsense_audio.py deleted file mode 100644 index 430ca5e..0000000 --- a/pygameControls/dualsense_audio.py +++ /dev/null @@ -1,70 +0,0 @@ -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})") - diff --git a/pygameControls/dualsense_controller.py b/pygameControls/dualsense_controller.py index c82353e..8a212f1 100644 --- a/pygameControls/dualsense_controller.py +++ b/pygameControls/dualsense_controller.py @@ -18,6 +18,27 @@ class DualSenseController(ControlsBase): self.powerlevel = self.device.battery.Level self.batterystate = BATTERY_STATE[str(self.device.battery.State)] self.set_player_id(PlayerID.PLAYER_1) + self.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[3], + "right stick y": self.axis[4], + "right trigger": self.buttons[5], + "left trigger": self.buttons[2], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[4], + "right button": self.buttons[5], + "cross button": self.buttons[0], + "triangle button": self.buttons[2], + "circle button": self.buttons[1], + "square button": self.buttons[3], + "left stick button": self.buttons[11], + "right stick button": self.buttons[12], + "connect button": self.buttons[8], + "list button": self.buttons[9], + "logo button": self.buttons[10] + } print(f"{self.name} connected") print(f"Power level: {self.powerlevel}") print(f"Battery state: {self.batterystate}") diff --git a/pygameControls/dualsense_edge_controller.py b/pygameControls/dualsense_edge_controller.py index 46bfb50..ecfb82d 100644 --- a/pygameControls/dualsense_edge_controller.py +++ b/pygameControls/dualsense_edge_controller.py @@ -1,199 +1,87 @@ -import time -import threading -import numpy as np -import sounddevice as sd -import alsaaudio -import pulsectl +from pygameControls.controlsbase import ControlsBase 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__": +class DualSenseEdgeController(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) + self.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[3], + "right stick y": self.axis[4], + "right trigger": self.buttons[5], + "left trigger": self.buttons[2], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[4], + "right button": self.buttons[5], + "cross button": self.buttons[0], + "triangle button": self.buttons[2], + "circle button": self.buttons[1], + "square button": self.buttons[3], + "left stick button": self.buttons[11], + "right stick button": self.buttons[12], + "connect button": self.buttons[8], + "list button": self.buttons[9], + "logo button": self.buttons[10] + } + print(f"{self.name} connected") + print(f"Power level: {self.powerlevel}") + print(f"Battery state: {self.batterystate}") - controller = DualSenseController() + def handle_input(self, event): + pass + + def set_led(self, red: int, green: int, blue: int): + self.device.light.setColorI(red, green, blue) - # 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() + 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 \ No newline at end of file diff --git a/pygameControls/generic_controller.py b/pygameControls/generic_controller.py index db9c6ab..2fd3d3b 100644 --- a/pygameControls/generic_controller.py +++ b/pygameControls/generic_controller.py @@ -13,6 +13,28 @@ class GenericController(ControlsBase): 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.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[2], + "right stick y": self.axis[3], + "right trigger": self.buttons[7], + "left trigger": self.buttons[6], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[4], + "right button": self.buttons[5], + "X button": self.buttons[0], + "Y button": self.buttons[3], + "A button": self.buttons[1], + "B button": self.buttons[2], + "left stick button": self.buttons[10], + "right stick button": self.buttons[11], + "back button": self.buttons[8], + "start button": self.buttons[9], + "logo button": None + } + print(f"{self.name} connected.") def handle_input(self, event): pass diff --git a/pygameControls/logitech_dual_action_controller.py b/pygameControls/logitech_dual_action_controller.py index f615236..edfe2fa 100644 --- a/pygameControls/logitech_dual_action_controller.py +++ b/pygameControls/logitech_dual_action_controller.py @@ -34,7 +34,29 @@ class LogitechDualActionController(ControlsBase): 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 - + self.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[2], + "right stick y": self.axis[3], + "right trigger": self.buttons[7], + "left trigger": self.buttons[6], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[4], + "right button": self.buttons[5], + "X button": self.buttons[0], + "Y button": self.buttons[3], + "A button": self.buttons[1], + "B button": self.buttons[2], + "left stick button": self.buttons[10], + "right stick button": self.buttons[11], + "back button": self.buttons[8], + "start button": self.buttons[9], + "logo button": None + } + print(f"{self.name} connected.") + def handle_input(self, event): pass diff --git a/pygameControls/logitech_f310_controller.py b/pygameControls/logitech_f310_controller.py index 02ad229..4e5d7a4 100644 --- a/pygameControls/logitech_f310_controller.py +++ b/pygameControls/logitech_f310_controller.py @@ -34,6 +34,28 @@ class LogitechF310Controller(ControlsBase): 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 + self.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[3], + "right stick y": self.axis[4], + "right trigger": self.axis[2], + "left trigger": self.axis[5], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[4], + "right button": self.buttons[5], + "X button": self.buttons[2], + "Y button": self.buttons[3], + "A button": self.buttons[0], + "B button": self.buttons[1], + "left stick button": self.buttons[9], + "right stick button": self.buttons[10], + "back button": self.buttons[6], + "start button": self.buttons[7], + "logo button": self.buttons[8] + } + print(f"{self.name} connected.") def handle_input(self, event): pass diff --git a/pygameControls/logitech_f510_controller.py b/pygameControls/logitech_f510_controller.py index 8aef7b5..d10f09b 100644 --- a/pygameControls/logitech_f510_controller.py +++ b/pygameControls/logitech_f510_controller.py @@ -36,7 +36,28 @@ class LogitechF510Controller(ControlsBase): self.numbuttons: int = self.device.get_numbuttons() self.buttons: list = [] self.input_mode: InputMode.DirectInput - self.input_connection: ConnectionType.WIRED + self.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[3], + "right stick y": self.axis[4], + "right trigger": self.axis[2], + "left trigger": self.axis[5], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[4], + "right button": self.buttons[5], + "X button": self.buttons[2], + "Y button": self.buttons[3], + "A button": self.buttons[0], + "B button": self.buttons[1], + "left stick button": self.buttons[9], + "right stick button": self.buttons[10], + "back button": self.buttons[6], + "start button": self.buttons[7], + "logo button": self.buttons[8] + } + print(f"{self.name} connected.") def handle_input(self, event): pass diff --git a/pygameControls/logitech_f710_controller.py b/pygameControls/logitech_f710_controller.py index 2449b92..f7c87eb 100644 --- a/pygameControls/logitech_f710_controller.py +++ b/pygameControls/logitech_f710_controller.py @@ -36,7 +36,28 @@ class LogitechF710Controller(ControlsBase): self.numbuttons: int = self.device.get_numbuttons() self.buttons: list = [] self.input_mode: InputMode.DirectInput - self.input_connection: ConnectionType.WIRED + self.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[3], + "right stick y": self.axis[4], + "right trigger": self.axis[2], + "left trigger": self.axis[5], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[4], + "right button": self.buttons[5], + "X button": self.buttons[2], + "Y button": self.buttons[3], + "A button": self.buttons[0], + "B button": self.buttons[1], + "left stick button": self.buttons[9], + "right stick button": self.buttons[10], + "back button": self.buttons[6], + "start button": self.buttons[7], + "logo button": self.buttons[8] + } + print(f"{self.name} connected.") def handle_input(self, event): pass diff --git a/pygameControls/xbox_series_x_controller.py b/pygameControls/xbox_series_x_controller.py index dab31ce..861373f 100644 --- a/pygameControls/xbox_series_x_controller.py +++ b/pygameControls/xbox_series_x_controller.py @@ -13,6 +13,29 @@ class XboxSeriesXController: 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.mapping = { + "left stick x": self.axis[0], + "left stick y": self.axis[1], + "right stick x": self.axis[2], + "right stick y": self.axis[3], + "right trigger": self.axis[4], + "left trigger": self.axis[5], + "dhat x": self.hats[0][0], + "dhat y": self.hats[0][1], + "left button": self.buttons[6], + "right button": self.buttons[7], + "X button": self.buttons[3], + "Y button": self.buttons[4], + "A button": self.buttons[0], + "B button": self.buttons[1], + "left stick button": self.buttons[13], + "right stick button": self.buttons[14], + "logo button": self.buttons[12], + "share button": self.buttons[15], + "list button": self.buttons[11], + "copy button": self.buttons[10] + } + print(f"{self.name} connected.") def handle_input(self, event): pass