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: