Skip to content

Commit f02d0db

Browse files
committed
Working with Pi
1 parent 8d62b54 commit f02d0db

File tree

9 files changed

+274
-146
lines changed

9 files changed

+274
-146
lines changed

config/controller.yml

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
controller:
22
enabled: true
33
path: 'modules.controller.Controller'
4-
dependencies:
5-
python:
6-
- inputs
7-
# sudo systemctl enable xboxdrv.service
8-
#
9-
unix:
10-
- xboxdrv
114
config:
125
deadzone: 500
6+
modifier_buttons:
7+
- BTN_TL
8+
- BTN_TR
139
button_action_map:
1410
default:
1511
BTN_TL:
@@ -36,16 +32,16 @@ controller:
3632
ABS_HAT0Y: # DPAD Y (-1, 0 or 1)
3733
- topic: ''
3834
args: {}
39-
BTN_SOUTH: # A
35+
BTN_A:
4036
- topic: 'tts'
4137
msg: 'Test Default'
42-
BTN_NORTH: # X
38+
BTN_X:
4339
- topic: 'eye/blink'
4440
args: {}
45-
BTN_WEST: # Y
46-
- topic: ''
41+
BTN_Y:
42+
- topic: 'gpio/laser/toggle'
4743
args: {}
48-
BTN_EAST: # B
44+
BTN_B:
4945
- topic: ''
5046
args: {}
5147
ABS_RX:
@@ -60,23 +56,23 @@ controller:
6056
modifier:
6157
scale: 1.0
6258
ABS_X:
63-
- topic: 'eye/look'
64-
args: {axis: 'x'}
59+
- topic: 'eye/move'
60+
args: {axis: 'y'}
6561
modifier:
66-
scale: 0.03
62+
scale: -0.03
6763
ABS_Y:
68-
- topic: 'eye/look'
69-
args: {axis: 'y'}
64+
- topic: 'eye/move'
65+
args: {axis: 'x'}
7066
modifier:
71-
scale: 0.03
67+
scale: -0.03
7268
ABS_Z: # Left trigger
7369
- topic: ''
7470
args: {}
7571
ABS_RZ: # Right trigger
7672
- topic: ''
7773
args: {}
78-
BTN_BL:
79-
BTN_SOUTH:
74+
BTN_TR:
75+
BTN_A:
8076
- topic: 'tts'
8177
msg: 'Left Shoulder Held'
8278
ABS_X:
@@ -97,4 +93,4 @@ controller:
9793
scale: 1.0
9894
- topic: 'servo:leg_r_hip:mv'
9995
modifier:
100-
scale: -1.0
96+
scale: -1.0

config/gpio_laser.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
motion:
2-
enabled: false
2+
enabled: true
33
path: 'modules.gpio.laser.Laser'
44
config:
55
pin: 6

config/tft_display.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
tft_display:
2-
enabled: false
2+
enabled: true
33
path: modules.display.tft_display_eye.TFTDisplayEye
44
config:
55
bus: 0 # Use `ls /dev/spi*` to identify. (/dev/spidev0.0) is bus 0, device 0
@@ -8,7 +8,7 @@ tft_display:
88
dc_pin: 25
99
bl_pin: 18 # Not used if no backlight pin on device
1010
rotation: -90.0
11-
test_on_boot: false
11+
test_on_boot: true
1212
colors:
1313
red: (255, 32, 32)
1414
blue: (0, 245, 255)

docs/Controller.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,59 @@ The `Controller` module provides an interface for using a game controller (such
66

77
## Enable controller on ubuntu:
88

9-
<!-- sudo apt install dkms linux-headers-$(uname -r)
9+
I'm not sure what helped with this situation, but I had immense difficulty getting bluetooth working for the Xbox Series S/X controller. I had to upgrade the firmware using this tool on windows 10: https://apps.microsoft.com/detail/9nblggh30xj3?hl=en-GB&gl=GB
10+
11+
In addition, I installed xpadneo:
12+
13+
```
14+
sudo apt install dkms linux-headers-$(uname -r)
1015
git clone https://github.com/atar-axis/xpadneo.git
1116
cd xpadneo
12-
sudo ./install.sh -->
13-
<!-- Did not work -->
17+
sudo ./install.sh
18+
```
19+
20+
Disabling ERTM may also have been required:
21+
```
22+
sudo nano /etc/modprobe.d/bluetooth.conf
23+
24+
# Add this line and save
25+
options bluetooth disable_ertm=Y
26+
27+
sudo reboot
28+
```
29+
30+
Then use bluetoothctl to test:
31+
```
32+
sudo bluetoothctl
33+
# Turn on agent and set as default
34+
agent on
35+
default-agent
36+
37+
# Start scanning for devices
38+
scan on
39+
40+
# Replace XX:XX:XX:XX:XX:XX with your controller's MAC address
41+
pair XX:XX:XX:XX:XX:XX
42+
trust XX:XX:XX:XX:XX:XX
43+
connect XX:XX:XX:XX:XX:X
44+
45+
exit
46+
```
47+
48+
Further adjustments that may have helped:
49+
50+
```
51+
# edit /etc/bluetooth/main.conf
52+
[General]
53+
ControllerMode = dual
54+
JustWorksRepairing = confirm
55+
```
56+
57+
The package xboxdrv was also suggested, but I couldn't get that to detect the controller, even once the script was working...
58+
59+
Testing with `jstest /dev/input/js0` (requires `sudo apt-get install joystick`) showed the controller worked, but I could not get the python modules `inputs` or `pygame` to recognise any input from the controller.
60+
61+
In the end I polled the /dev/input/js0 stream into python.
1462

1563
## Configuration
1664

modules/controller.py

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
from modules.base_module import BaseModule
44
import yaml
55

6-
try:
7-
import inputs # pip install inputs
8-
except ImportError:
9-
raise ImportError("Please install the 'inputs' package: pip install inputs")
6+
import struct
107

118
class Controller(BaseModule):
129
def __init__(self, **kwargs):
@@ -19,21 +16,121 @@ def __init__(self, **kwargs):
1916
self.pressed_buttons = set()
2017
self.axis_last_value = {} # Track last value for each axis
2118
self.axis_last_time = {} # Track last event time (ms) for each axis
19+
self.device = kwargs.get('device', "/dev/input/js0")
2220

2321
def setup_messaging(self):
2422
self.running = True
2523
self.thread.start()
2624

2725
def listen(self):
2826
self.log("Controller listening for input...")
27+
JS_EVENT_FORMAT = "IhBB"
28+
JS_EVENT_SIZE = struct.calcsize(JS_EVENT_FORMAT)
2929
while self.running:
3030
try:
31-
events = inputs.get_gamepad()
32-
for event in events:
33-
self.handle_event(event)
34-
except inputs.UnpluggedError:
35-
self.log("No gamepad found. Waiting for connection...", level='warning')
31+
with open(self.device, "rb") as jsdev:
32+
while self.running:
33+
evbuf = jsdev.read(JS_EVENT_SIZE)
34+
if not evbuf:
35+
continue
36+
time_ms, value, type_, number = struct.unpack(JS_EVENT_FORMAT, evbuf)
37+
# print(f"time={time} value={value} type={type_} number={number}")
38+
self.handle_js_event(time_ms, value, type_, number)
39+
except FileNotFoundError:
40+
self.log(f"No gamepad found at {self.device}. Waiting for connection...", level='warning')
3641
time.sleep(1)
42+
except Exception as e:
43+
self.log(f"Error reading {self.device}: {e}", level='error')
44+
time.sleep(1)
45+
46+
def handle_js_event(self, time_ms, value, type_, number):
47+
# type_: 0x01 = button, 0x02 = axis, 0x80 = init
48+
JS_EVENT_BUTTON = 1
49+
JS_EVENT_AXIS = 0x02
50+
JS_EVENT_INIT = 0x80
51+
52+
is_init = (type_ & JS_EVENT_INIT) != 0
53+
event_type = type_ & ~JS_EVENT_INIT
54+
55+
# Map button/axis numbers to names as needed for your controller
56+
button_names = {
57+
0: 'BTN_A', 1: 'BTN_B', 2: 'BTN_X', 3: 'BTN_Y',
58+
4: 'BTN_TL', 5: 'BTN_TR', 6: 'BTN_SELECT', 7: 'BTN_START',
59+
8: 'BTN_MODE', 9: 'BTN_THUMBL', 10: 'BTN_THUMBR'
60+
}
61+
axis_names = {
62+
0: 'ABS_X', 1: 'ABS_Y', 2: 'ABS_Z', 3: 'ABS_RZ',
63+
4: 'ABS_RX', 5: 'ABS_RY', 6: 'ABS_HAT0X', 7: 'ABS_HAT0Y'
64+
}
65+
66+
# self.log(f"JS Event: time={time_ms} value={value} type={event_type} number={number} (init={is_init})")
67+
68+
if event_type == JS_EVENT_BUTTON:
69+
# self.log(f"JS Button Event: number={number} value={value} (init={is_init})")
70+
button = button_names.get(number, f'BTN_{number}')
71+
self.log(f"Button event: {button} value={value} (jsdev)")
72+
# value: 1=pressed, 0=released
73+
if value == 1:
74+
self.pressed_buttons.add(button)
75+
elif value == 0 and button in self.pressed_buttons:
76+
self.pressed_buttons.remove(button)
77+
78+
active_mods = sorted([b for b in self.pressed_buttons if b in self.modifier_buttons])
79+
mapping_key = '+'.join(active_mods) if active_mods else 'default'
80+
self.log(f"Active modifiers: {active_mods} (jsdev)")
81+
button_map = self.button_action_map.get(mapping_key, self.button_action_map.get('default', {}))
82+
83+
self.log(button)
84+
if button in button_map and value == 1:
85+
actions = button_map[button]
86+
for mapping in actions:
87+
topic = mapping.get('topic')
88+
if not topic:
89+
self.log(f"Empty topic for button {button} mapping, skipping.", level='warning')
90+
continue # Skip empty topic
91+
args = mapping.get('args', {})
92+
self.publish(topic, **args)
93+
self.log(f"Published to topic {topic} with args {args} (jsdev)")
94+
95+
elif event_type == JS_EVENT_AXIS:
96+
axis = axis_names.get(number, f'AXIS_{number}')
97+
# self.log(f"Axis event: {axis} value={value} (jsdev)")
98+
# value: -32767..32767 for sticks, -1/0/1 for hats
99+
active_mods = sorted([b for b in self.pressed_buttons if b in self.modifier_buttons])
100+
mapping_key = '+'.join(active_mods) if active_mods else 'default'
101+
button_map = self.button_action_map.get(mapping_key, self.button_action_map.get('default', {}))
102+
self.log(f"Active modifiers: {active_mods} (jsdev)")
103+
104+
if axis in button_map:
105+
actions = button_map[axis]
106+
for mapping in actions:
107+
deadzone = mapping.get('deadzone', self.global_deadzone)
108+
if deadzone > abs(value):
109+
continue # Skip if within deadzone
110+
111+
debounce = mapping.get('debounce', self.global_debounce) # debounce in ms
112+
now = int(time.time() * 1000) # current time in ms
113+
last_time = self.axis_last_time.get(axis, None)
114+
115+
# Only allow if enough time has passed
116+
if last_time is not None and (now - last_time) < debounce:
117+
continue # Skip if within debounce interval
118+
self.axis_last_time[axis] = now
119+
last_value = self.axis_last_value.get(axis, 0)
120+
delta = value - last_value
121+
self.axis_last_value[axis] = value
122+
123+
topic = mapping.get('topic')
124+
if not topic:
125+
continue # Skip empty topic
126+
args = dict(mapping.get('args', {})) # Copy to avoid mutation
127+
modifier = mapping.get('modifier', None)
128+
if modifier is not None:
129+
scale = modifier.get('scale', 1.0)
130+
args['delta'] = delta * scale
131+
# self.log(f"Axis {axis} moved to {args.get('delta', value)} (jsdev)")
132+
self.publish(topic, **args)
133+
self.log(f"Published to topic {topic} with args {args} (jsdev)")
37134

38135
def handle_event(self, event):
39136
# if event.code == 'ABS_Y':

0 commit comments

Comments
 (0)