Skip to content

Commit 094a185

Browse files
committed
Add support for multiple joysticks
1 parent 94dba45 commit 094a185

File tree

5 files changed

+72
-50
lines changed

5 files changed

+72
-50
lines changed

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ This utility can be considered a breach of TOS for some games, please make sure
99
## Features
1010

1111
- Maps joystick buttons and hat directions to keyboard sequences
12+
- Supports multiple joysticks and controllers simultaneously
1213
- Supports modifier buttons (combinations)
1314
- Configurable delays between key presses
1415
- Supports special wait commands in sequences
1516
- Can execute pre-run and post-run functions
1617
- Includes a helper to identify joystick buttons and keyboard keys
1718
- Configuration via INI file or Python dictionary
1819
- Runs in the system tray with options to reload configuration and exit
19-
-
20+
2021
### Power Configuration Presets (PIPS)
2122

2223
The default configuration is set up for Elite Dangerous
@@ -76,7 +77,7 @@ You can see how it was created in the [Action tab](https://github.com/bruj0/ed-j
7677

7778
- Python 3.6+
7879
- Windows operating system
79-
- A joystick or controller
80+
- One or more joysticks or controllers
8081
- Elite Dangerous game (optional, but that's what it's designed for)
8182

8283
### Setup
@@ -116,13 +117,13 @@ This will create a `config.ini` file with the default PIPS (Power Distribution)
116117

117118
### Identifying Joystick Buttons
118119

119-
To see which buttons on your controller correspond to which button IDs:
120+
To see which buttons on your controller correspond to which button IDs (including support for multiple joysticks):
120121

121122
```bash
122123
python main.py --joystick-events
123124
```
124125

125-
Press buttons on your joystick or move the hat to see the output. Use these names in your configuration.
126+
Press buttons on your joystick(s) or move the hat to see the output. The output will indicate both the button/hat and the joystick index (e.g., `BUTTON_0_JOY0`, `HAT_0_JOY1_up`). Use these names in your configuration to assign actions to specific devices.
126127

127128
### Identifying Keyboard Keys
128129

@@ -136,22 +137,24 @@ Press keys on your keyboard to see their names. Use these names in your configur
136137

137138
## Configuration
138139

139-
You can configure the application using an INI file. Here's an example of the default configuration for Elite Dangerous power management:
140+
You can configure the application using an INI file. Here's an example of the default configuration for Elite Dangerous power management (for the first joystick):
140141

141142
```ini
142-
[HAT_0_up]
143+
[HAT_0_JOY0_up]
143144
sequence = [{"key": "v", "presses": 1}, {"key": "x", "presses": 2}]
144145

145-
[HAT_0_down]
146+
[HAT_0_JOY0_down]
146147
sequence = [{"key": "v", "presses": 1}, {"key": "c", "presses": 2}]
147148

148-
[HAT_0_left]
149+
[HAT_0_JOY0_left]
149150
sequence = [{"key": "v", "presses": 1}, {"key": "z", "presses": 1}, {"key": "x", "presses": 1}]
150151

151-
[HAT_0_right]
152+
[HAT_0_JOY0_right]
152153
sequence = [{"key": "v", "presses": 1}, {"key": "c", "presses": 1}, {"key": "x", "presses": 1}]
153154
```
154155

156+
To configure actions for additional joysticks, use the appropriate joystick index in the section name (e.g., `[BUTTON_5_JOY1]`).
157+
155158
### Power Configuration Presets (PIPS)
156159

157160
The default configuration is set up for Elite Dangerous power distribution using the hat switch:

config.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
[HAT_0_up]
1+
[HAT_0_JOY0_up]
22
sequence = [{'key': 'v', 'presses': 1}, {'key': 'x', 'presses': 2}]
33

4-
[HAT_0_down]
4+
[HAT_0_JOY0_down]
55
sequence = [{'key': 'v', 'presses': 1}, {'key': 'c', 'presses': 2}]
66

7-
[HAT_0_left]
7+
[HAT_0_JOY0_left]
88
sequence = [{'key': 'v', 'presses': 1}, {'key': 'z', 'presses': 1}, {'key': 'x', 'presses': 1}]
99

10-
[HAT_0_right]
10+
[HAT_0_JOY0_right]
1111
sequence = [{'key': 'v', 'presses': 1}, {'key': 'c', 'presses': 1}, {'key': 'x', 'presses': 1}]
1212

1313
[BUTTON_27]

ed_joystick_helper.py

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ def __init__(self, config):
2424
self.pressed_buttons = set()
2525
self.current_hat_positions = {} # Track current hat positions
2626
self.config_file_path = None # Store config file path for reloading
27-
27+
2828
# Set up logging
2929
self.logger = logging.getLogger("ED_Joystick_Helper")
3030
if not self.logger.handlers:
3131
# Only configure if not already configured
3232
handler = logging.FileHandler("ed_joystick_helper.log")
33-
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
33+
formatter = logging.Formatter(
34+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
35+
)
3436
handler.setFormatter(formatter)
3537
self.logger.addHandler(handler)
36-
self.logger.setLevel(logging.INFO)
38+
self.logger.setLevel(logging.DEBUG)
3739

3840
# Initialize pygame for joystick support
3941
pygame.init()
@@ -73,7 +75,7 @@ def _execute_sequence(self, button_name, button_config):
7375
for item in sequence:
7476
key = item["key"]
7577
presses = item["presses"]
76-
78+
7779
if key == "WAIT":
7880
# Special case for wait command
7981
time.sleep(presses)
@@ -105,11 +107,15 @@ def _map_key(self, key_name):
105107
# Otherwise, assume it's a character
106108
return key_name
107109

108-
def _process_button_press(self, button_name):
109-
"""Process a button press, checking modifiers and executing sequences"""
110-
self.logger.info(f"Button pressed: {button_name}")
111-
if button_name in self.config:
112-
button_config = self.config[button_name]
110+
def _process_button_press(self, button_name, joy_id=None):
111+
"""Process a button press, checking modifiers and executing sequences. Supports multiple joysticks with same button name."""
112+
# If joy_id is provided, use extended button name
113+
config_key = button_name
114+
if joy_id is not None:
115+
config_key = f"{button_name}_JOY{joy_id}"
116+
self.logger.debug(f"Button pressed: {config_key}")
117+
if config_key in self.config:
118+
button_config = self.config[config_key]
113119

114120
# Check if this button requires a modifier
115121
if "modifier" in button_config:
@@ -119,11 +125,11 @@ def _process_button_press(self, button_name):
119125

120126
# Execute the sequence in a separate thread to not block the event loop
121127
threading.Thread(
122-
target=self._execute_sequence, args=(button_name, button_config)
128+
target=self._execute_sequence, args=(config_key, button_config)
123129
).start()
124130

125-
def _process_hat_event(self, hat_id, x_value, y_value):
126-
"""Process a HAT event, converting it to a direction and triggering actions"""
131+
def _process_hat_event(self, hat_id, x_value, y_value, joy_id=None):
132+
"""Process a HAT event, converting it to a direction and triggering actions. Supports multiple joysticks."""
127133
# Map hat position to direction
128134
direction = "centered"
129135
if x_value == 0 and y_value == 1:
@@ -144,9 +150,11 @@ def _process_hat_event(self, hat_id, x_value, y_value):
144150
direction = "up-left"
145151

146152
hat_name = f"HAT_{hat_id}"
153+
if joy_id is not None:
154+
hat_name = f"{hat_name}_JOY{joy_id}"
147155

148156
# Only process if the direction has changed
149-
if self.current_hat_positions[hat_name] != direction:
157+
if self.current_hat_positions.get(hat_name, None) != direction:
150158
self.current_hat_positions[hat_name] = direction
151159

152160
# Create a hat event identifier that includes the direction
@@ -168,6 +176,7 @@ def reload_config(self):
168176
try:
169177
# Import here to avoid circular imports
170178
from main import load_config_from_ini
179+
171180
new_config = load_config_from_ini(self.config_file_path)
172181
self.config = new_config
173182
self.logger.info(f"Configuration reloaded from {self.config_file_path}")
@@ -188,16 +197,20 @@ def start(self):
188197
# self.logger.debug(f"Event: {event}")
189198
if event.type == pygame.JOYBUTTONDOWN:
190199
button_name = f"BUTTON_{event.button}"
191-
self.pressed_buttons.add(button_name)
192-
self._process_button_press(button_name)
200+
joy_id = event.joy if hasattr(event, 'joy') else 0
201+
self.pressed_buttons.add(f"{button_name}_JOY{joy_id}")
202+
self._process_button_press(button_name, joy_id)
193203
elif event.type == pygame.JOYBUTTONUP:
194204
button_name = f"BUTTON_{event.button}"
195-
if button_name in self.pressed_buttons:
196-
self.pressed_buttons.remove(button_name)
205+
joy_id = event.joy if hasattr(event, 'joy') else 0
206+
pressed_name = f"{button_name}_JOY{joy_id}"
207+
if pressed_name in self.pressed_buttons:
208+
self.pressed_buttons.remove(pressed_name)
197209
elif event.type == pygame.JOYHATMOTION:
198210
hat_id = event.hat
199211
x_value, y_value = event.value
200-
self._process_hat_event(hat_id, x_value, y_value)
212+
joy_id = event.joy if hasattr(event, 'joy') else 0
213+
self._process_hat_event(hat_id, x_value, y_value, joy_id)
201214
elif event.type == pygame.KEYDOWN:
202215
key_name = pygame.key.name(event.key)
203216
key_id = (
@@ -233,14 +246,16 @@ def print_joystick_events():
233246
logging.basicConfig(
234247
filename="ed_joystick_helper.log",
235248
level=logging.INFO,
236-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
249+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
237250
)
238-
251+
239252
pygame.init()
240253
pygame.joystick.init()
241254

242255
# Enable joystick events
243-
pygame.event.set_allowed([pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP, pygame.JOYHATMOTION])
256+
pygame.event.set_allowed(
257+
[pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP, pygame.JOYHATMOTION]
258+
)
244259

245260
# Get the number of joysticks
246261
joystick_count = pygame.joystick.get_count()
@@ -268,10 +283,12 @@ def print_joystick_events():
268283
# logger.debug(f"Event: {event}") # Uncomment to see all events for debugging
269284
if event.type == pygame.JOYBUTTONDOWN:
270285
button_name = f"BUTTON_{event.button}"
271-
logger.info(f"Button pressed: {button_name}")
286+
joy_id = event.joy if hasattr(event, 'joy') else 0
287+
logger.info(f"Button pressed: {button_name}_JOY{joy_id}")
272288
elif event.type == pygame.JOYHATMOTION:
273289
hat_id = event.hat
274290
x_value, y_value = event.value
291+
joy_id = event.joy if hasattr(event, 'joy') else 0
275292
direction = "centered"
276293

277294
# Map hat position to direction
@@ -292,7 +309,7 @@ def print_joystick_events():
292309
elif x_value == -1 and y_value == 1:
293310
direction = "up-left"
294311

295-
hat_name = f"HAT_{hat_id}"
312+
hat_name = f"HAT_{hat_id}_JOY{joy_id}"
296313
logger.info(
297314
f"Hat moved: {hat_name}, "
298315
f"Position: {direction} ({x_value}, {y_value})"
@@ -313,9 +330,9 @@ def print_keyboard_events():
313330
logging.basicConfig(
314331
filename="ed_joystick_helper.log",
315332
level=logging.INFO,
316-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
333+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
317334
)
318-
335+
319336
pygame.init()
320337
pygame.joystick.init()
321338

@@ -340,7 +357,9 @@ def print_keyboard_events():
340357
if key_name == " ":
341358
logger.info("Key pressed: 'SPACE' - Config as: KEY_SPACE")
342359
elif len(key_name) == 1:
343-
logger.info(f"Key pressed: '{key_name}' - Config as: {key_name}")
360+
logger.info(
361+
f"Key pressed: '{key_name}' - Config as: {key_name}"
362+
)
344363
else:
345364
key_upper = key_name.upper()
346365
logger.info(

ed_joystick_tray.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def setup_logging():
1717
log_file = "ed_joystick_helper.log"
1818
logging.basicConfig(
1919
filename=log_file,
20-
level=logging.INFO,
20+
level=logging.DEBUG,
2121
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
2222
datefmt="%Y-%m-%d %H:%M:%S",
2323
)

main.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,29 @@
2121
def setup_normal_logging():
2222
"""Set up logging to file"""
2323
logging.basicConfig(
24-
level=logging.INFO,
24+
level=logging.DEBUG,
2525
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
2626
datefmt="%Y-%m-%d %H:%M:%S",
2727
)
28-
logger = logging.getLogger("ED_Joystick_Helper")
28+
logger = logging.getLogger("main")
2929
return logger
3030

3131

3232
def print_starting(caller):
3333
"""Example pre-run function"""
34-
logger = logging.getLogger("ED_Joystick_Helper")
34+
logger = logging.getLogger("main")
3535
logger.info(f"Sequence for {caller} started")
3636

3737

3838
def print_end(caller):
3939
"""Example after-run function"""
40-
logger = logging.getLogger("ED_Joystick_Helper")
40+
logger = logging.getLogger("main")
4141
logger.info(f"Sequence for {caller} ended")
4242

4343

4444
def load_config_from_ini(file_path):
4545
"""Load configuration from an INI file"""
46-
logger = logging.getLogger("ED_Joystick_Helper")
46+
logger = logging.getLogger("main")
4747
config = configparser.ConfigParser()
4848
if not os.path.exists(file_path):
4949
logger.error(f"Configuration file {file_path} not found.")
@@ -88,7 +88,7 @@ def load_config_from_ini(file_path):
8888

8989
def create_default_config_file(file_path, config_dict):
9090
"""Create a default INI configuration file"""
91-
logger = logging.getLogger("ED_Joystick_Helper")
91+
logger = logging.getLogger("main")
9292
config = configparser.ConfigParser()
9393

9494
for button, settings in config_dict.items():
@@ -116,21 +116,21 @@ def create_default_config_file(file_path, config_dict):
116116

117117

118118
default_config = {
119-
"HAT_0_up": {
119+
"HAT_0_JOY0_up": {
120120
# Combat 1SYS/1ENG/4WEP
121121
"sequence": [
122122
{"key": "v", "presses": 1}, # Reset PIPS
123123
{"key": "x", "presses": 2}, # 4WEP
124124
]
125125
},
126-
"HAT_0_down": {
126+
"HAT_0_JOY0_down": {
127127
# Shields 4SYS/2ENG
128128
"sequence": [
129129
{"key": "v", "presses": 1},
130130
{"key": "c", "presses": 2} # 4SYS
131131
]
132132
},
133-
"HAT_0_left": {
133+
"HAT_0_JOY0_left": {
134134
# Persuit 2ENG/4WEP
135135
"sequence": [
136136
{"key": "v", "presses": 1},
@@ -139,7 +139,7 @@ def create_default_config_file(file_path, config_dict):
139139
]
140140
},
141141
# Offense 3SYS/1ENG/3WEP
142-
"HAT_0_right": {
142+
"HAT_0_JOY0_right": {
143143
"sequence": [
144144
{"key": "v", "presses": 1},
145145
{"key": "c", "presses": 1}, # 3SYS

0 commit comments

Comments
 (0)