diff --git a/.gitignore b/.gitignore index 3a9e9e5..17c433c 100644 --- a/.gitignore +++ b/.gitignore @@ -168,4 +168,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -testing/ +testing/* diff --git a/README.md b/README.md index 8fc5cac..cbd5c63 100644 --- a/README.md +++ b/README.md @@ -25,5 +25,6 @@ Højre - d eller Venstre - a eller Op - w eller Ned - s eller +Pause - p Alternativt, kan et joystick :joystick: eller gamepad :video_game: bruges. \ No newline at end of file diff --git a/controls/controller.py b/controls/controller.py index a1fddee..f842f67 100644 --- a/controls/controller.py +++ b/controls/controller.py @@ -1,76 +1,27 @@ import pygame from controls.controlsbase import ControlsBase +from controls.dualsense_controller import DualSenseController +from controls.dualsense_edge_controller import DualSenseEdgeController +from controls.logitech_f310_controller import LogitechF310Controller +from controls.logitech_f510_controller import LogitechF510Controller +from controls.logitech_f710_controller import LogitechF710Controller +from controls.xbox_controller import XboxController +from controls.generic_controller import GenericController -class ControllerControls(ControlsBase): +CONTROLLERS = { + "DualSense Wireless Controller": DualSenseController, + "DualSense Edge Wireless Controller": DualSenseEdgeController, + "Logitech Gamepad F310": LogitechF310Controller, + "Logitech Gamepad F510": LogitechF510Controller, + "Logitech Gamepad F710": LogitechF710Controller, + "Xbox": XboxController + } + +class Controllers: 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.power_level: str = "" - - 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 power_level(self) -> str: - return self._power_level - - @power_level.setter - def power_level(self) -> None: - self._power_level = self.device.get_power_level() \ No newline at end of file + self.controllers = [] + if not joy.get_name() in CONTROLLERS: + self.controllers.append(GenericController(joy)) + else: + self.controllers.append(CONTROLLERS[joy.get_name()](joy)) + \ No newline at end of file diff --git a/controls/controlsbase.py b/controls/controlsbase.py index d6cd6ca..8285cae 100644 --- a/controls/controlsbase.py +++ b/controls/controlsbase.py @@ -6,4 +6,28 @@ 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 \ No newline at end of file diff --git a/controls/dualsense_audio.py b/controls/dualsense_audio.py new file mode 100644 index 0000000..430ca5e --- /dev/null +++ b/controls/dualsense_audio.py @@ -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})") + diff --git a/controls/dualsense_controller.py b/controls/dualsense_controller.py new file mode 100644 index 0000000..96c6e37 --- /dev/null +++ b/controls/dualsense_controller.py @@ -0,0 +1,199 @@ +import time +import threading +import numpy as np +import sounddevice as sd +import alsaaudio +import pulsectl +from pydualsense import * + + +class DualSenseController: + 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() diff --git a/controls/dualsense_edge_controller.py b/controls/dualsense_edge_controller.py new file mode 100644 index 0000000..46bfb50 --- /dev/null +++ b/controls/dualsense_edge_controller.py @@ -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() diff --git a/controls/generic_controller.py b/controls/generic_controller.py new file mode 100644 index 0000000..bbc0840 --- /dev/null +++ b/controls/generic_controller.py @@ -0,0 +1,68 @@ +import pygame +from controls.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.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 = [] + + 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)] + \ No newline at end of file diff --git a/controls/logitech_f310_controller.py b/controls/logitech_f310_controller.py new file mode 100644 index 0000000..c283d76 --- /dev/null +++ b/controls/logitech_f310_controller.py @@ -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 controls.controlsbase import ControlsBase +from enum import Enum + +class InputMode(Enum): + DirectInput = 1 + XInput = 2 + +class ConnectionType(Enum): + WIRED = 1 + WIRELESS = 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.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 \ No newline at end of file diff --git a/controls/logitech_f510_controller.py b/controls/logitech_f510_controller.py new file mode 100644 index 0000000..610f126 --- /dev/null +++ b/controls/logitech_f510_controller.py @@ -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 controls.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 \ No newline at end of file diff --git a/controls/logitech_f710_controller.py b/controls/logitech_f710_controller.py new file mode 100644 index 0000000..368aefe --- /dev/null +++ b/controls/logitech_f710_controller.py @@ -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 controls.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 \ No newline at end of file diff --git a/controls/xbox_controller.py b/controls/xbox_controller.py new file mode 100644 index 0000000..86c97af --- /dev/null +++ b/controls/xbox_controller.py @@ -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() diff --git a/main.py b/main.py index 3a57fa3..d5d7266 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,16 @@ import time import controls from screens.startscreen import StartScreen from tilemap.playground import PlayGround +from player.player import Player, Players +from enum import Enum + +class State(Enum): + STARTING = 1 + PLAYING = 2 + PAUSING = 3 + ENDING = 4 + SETTING = 5 + HIGHSCORE = 6 class Snake: def __init__(self): @@ -15,7 +25,7 @@ class Snake: self.windowed: bool = True self.width: int = 800 self.height: int =600 - self.startscreen = None + self.screen = None self.icon = pygame.image.load("snake.webp") pygame.display.set_icon(self.icon) @@ -29,7 +39,20 @@ class Snake: while self.running: match self.state: case 0: - self.startscreen = StartScreen(self) + self.screen = StartScreen(self) + pygame.display.flip() + waiting = True + while waiting: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + waiting = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_F1: + print("F1 pressed!") + waiting = False + elif event.key == pygame.K_F2: + print("F2 pressed!") + waiting = False case 1: pass case 2: diff --git a/player/player.py b/player/player.py index 6a158db..2971db1 100644 --- a/player/player.py +++ b/player/player.py @@ -1,5 +1,27 @@ +from enum import Enum +from controls.controller import Controllers + +class Players(Enum): + UP1 = 1 + UP2 = 2 + class Player: def __init__(self): self.controls = None - \ No newline at end of file + @property + def player(self) -> int: + return self._player + + @player.setter + def player(self, plr: int) -> None: + self._player = plr + + @property + def controls(self) -> Controllers: + return self._controls + + @controls.setter + def controls(self, ctl: Controllers) -> None: + self._controls = ctl + \ No newline at end of file diff --git a/gamepad.png b/resources/gfx/gamepad.png similarity index 100% rename from gamepad.png rename to resources/gfx/gamepad.png diff --git a/joystick.png b/resources/gfx/joystick.png similarity index 100% rename from joystick.png rename to resources/gfx/joystick.png diff --git a/keyboard.png b/resources/gfx/keyboard.png similarity index 100% rename from keyboard.png rename to resources/gfx/keyboard.png diff --git a/logitech-t310.png b/resources/gfx/logitech-t310.png similarity index 100% rename from logitech-t310.png rename to resources/gfx/logitech-t310.png diff --git a/ps5-dualsense.png b/resources/gfx/ps5-dualsense.png similarity index 100% rename from ps5-dualsense.png rename to resources/gfx/ps5-dualsense.png diff --git a/ps5-dualsense.webp b/resources/gfx/ps5-dualsense.webp similarity index 100% rename from ps5-dualsense.webp rename to resources/gfx/ps5-dualsense.webp diff --git a/pygame_logo.png b/resources/gfx/pygame_logo.png similarity index 100% rename from pygame_logo.png rename to resources/gfx/pygame_logo.png diff --git a/screens/startscreen.py b/screens/startscreen.py index ce3e3c5..fd26fb4 100644 --- a/screens/startscreen.py +++ b/screens/startscreen.py @@ -8,4 +8,7 @@ class StartScreen: self.banner = pygame.transform.smoothscale(self.banner, (self.banner_width, int(self.banner_width*0.4))) self.parent.screen.blit(self.banner, (0, 0)) + + + \ No newline at end of file diff --git a/testing/controls.py b/testing/controls.py index 5f50919..c17d221 100644 --- a/testing/controls.py +++ b/testing/controls.py @@ -26,9 +26,9 @@ def load_image(path, max_size): new_size = (int(image_rect.width * scale_factor), int(image_rect.height * scale_factor)) return pygame.transform.smoothscale(image, new_size) -keyboard_image = load_image("keyboard.png", (100, 100)) +keyboard_image = load_image("resources/gfx/keyboard.png", (100, 100)) #joystick_image = load_image("ps5-dualsense.png", (100, 100)) -joystick_image = load_image("gamepad.png", (100, 100)) +joystick_image = load_image("resources/gfx/gamepad.png", (100, 100)) # Abstract Control Class class Control(ABC): diff --git a/testing/joystick.py b/testing/joystick.py index 7874915..7e31275 100644 --- a/testing/joystick.py +++ b/testing/joystick.py @@ -57,7 +57,7 @@ def main(): print("Joystick button pressed.") if event.button == 0: joystick = joysticks[event.instance_id] - if joystick.rumble(0, 0.7, 500): + if joystick.rumble(0.7, 1.0, 1000): print(f"Rumble effect played on joystick {event.instance_id}") if event.type == pygame.JOYBUTTONUP: