robot-xbox-controller 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ import math
2
+ import os
3
+ import pygame
4
+ import time
5
+
6
+ from robot.xbox_state import XboxState
7
+
8
+
9
+ class XboxController:
10
+ EVENT_LOOP_DELAY = 0.05
11
+ WAITING_LOOP_DELAY = 1.0
12
+ CLOCK_TICKS = 20
13
+
14
+ BUTTON_A = 0
15
+ BUTTON_B = 1
16
+ BUTTON_X = 2
17
+ BUTTON_Y = 3
18
+ BUTTON_VIEW = 4 # left middle button
19
+ BUTTON_XBOX = 5 # center middle button
20
+ BUTTON_SHARE = 6 # right middle button
21
+ BUTTON_LEFT_JOYSTICK = 7
22
+ BUTTON_RIGHT_JOYSTICK = 8
23
+ BUTTON_LEFT_BUMPER = 9
24
+ BUTTON_RIGHT_BUMPER = 10
25
+ BUTTON_FORWARD = 11
26
+ BUTTON_BACKWARD = 12
27
+ BUTTON_LEFT = 13
28
+ BUTTON_RIGHT = 14
29
+
30
+ def __init__(self):
31
+ self.connected = False
32
+ self.controller = None
33
+ self.controller_name = None
34
+ self.device_index = None
35
+ self.state = XboxState()
36
+ self.clock = pygame.time.Clock()
37
+
38
+ os.environ["SDL_VIDEODRIVER"] = "dummy"
39
+
40
+ pygame.init()
41
+ pygame.joystick.init()
42
+
43
+ def _connect_event(self, event):
44
+ self.device_index = event.device_index
45
+ self.controller = pygame.joystick.Joystick(self.device_index)
46
+ self.controller_name = self.controller.get_name()
47
+ self.connected = True
48
+
49
+ return self
50
+
51
+ def connect(self, wait_for_controller_message="Waiting for Xbox Controller"):
52
+ if wait_for_controller_message:
53
+ while True:
54
+ for event in pygame.event.get():
55
+ if event.type == pygame.JOYDEVICEADDED:
56
+ return self._connect_event(event)
57
+
58
+ print(wait_for_controller_message)
59
+
60
+ time.sleep(XboxController.WAITING_LOOP_DELAY)
61
+
62
+ @classmethod
63
+ def quit_event(cls, event):
64
+ if event.type == pygame.QUIT:
65
+ return True
66
+
67
+ if event.type == pygame.JOYBUTTONDOWN:
68
+ if event.button == 15:
69
+ return True
70
+
71
+ return False
72
+
73
+ def run(self, run_method, debugging=False):
74
+ running = True
75
+
76
+ while running:
77
+ for event in pygame.event.get():
78
+ if self.quit_event(event):
79
+ return False
80
+ else:
81
+ self.state.event(event, debugging)
82
+
83
+ running = run_method(self)
84
+
85
+ def tick(self, frame_rate):
86
+ self.clock.tick(frame_rate)
87
+
88
+ @classmethod
89
+ def wheel_speeds(cls, x_axis, y_axis, max_speed=1.0):
90
+ """
91
+ Converts controller axes to differential wheel speeds.
92
+ y_axis: forward/backward (-1 to 1)
93
+ x_axis: left/right (-1 to 1)
94
+ """
95
+ # Simple mapping:
96
+ # Forward: Y is negative, so invert it
97
+ # Turn: X is positive to the right
98
+
99
+ speed = -y_axis
100
+ turn = x_axis
101
+
102
+ # Calculate speeds
103
+ left_speed = speed + turn
104
+ right_speed = speed - turn
105
+
106
+ # Normalize/Clamp values to -1.0 to 1.0
107
+ left_speed = max(-1.0, min(1.0, left_speed)) * max_speed
108
+ right_speed = max(-1.0, min(1.0, right_speed)) * max_speed
109
+
110
+ return round(left_speed, 2), round(right_speed, 2)
111
+
112
+ @classmethod
113
+ def wheel_speed_and_angle(cls, x_axis, y_axis, max_speed=1.0):
114
+ if abs(x_axis) < 0.25:
115
+ x_axis = 0.0
116
+ if abs(y_axis) < 0.25:
117
+ y_axis = 0.0
118
+
119
+ speed = math.sqrt(x_axis**2 + y_axis**2)
120
+
121
+ if speed > 1.0:
122
+ speed = 1.0
123
+
124
+ angle_rad = math.atan2(-y_axis, x_axis)
125
+ angle_deg = math.degrees(angle_rad)
126
+
127
+ # if angle_deg < 0:
128
+ # angle_deg += 360
129
+
130
+ # print("Axis:", x_axis, y_axis, " speed:", speed, " angle", angle_deg)
131
+ return round(speed, 2), round(angle_deg, 2)
robot/xbox_state.py ADDED
@@ -0,0 +1,96 @@
1
+ import pygame
2
+ import time
3
+
4
+
5
+ class XboxState:
6
+ JOYSTICK_DRIFT = 0.18
7
+
8
+ def __init__(self):
9
+ self.axis_value = [0.0, 0.0, 0.0, 0.0, -1.0, -1.0]
10
+ self.button_down_at = [0] * 16
11
+ pass
12
+
13
+ def show_value_string(self, value):
14
+ return "{:6.2f}".format(value)
15
+
16
+ def state_string(self, extra_string=None):
17
+ s = ""
18
+ s = s + self.show_value_string(self.left_x())
19
+ s = s + self.show_value_string(self.left_y())
20
+ s = s + self.show_value_string(self.right_x())
21
+ s = s + self.show_value_string(self.right_y())
22
+ s = s + self.show_value_string(self.left_trigger())
23
+ s = s + self.show_value_string(self.right_trigger())
24
+ if extra_string is not None:
25
+ s = s + " " + extra_string
26
+
27
+ for button in range(16):
28
+ down_seconds = self.button_down_seconds(button)
29
+
30
+ if down_seconds > 0:
31
+ s += f" button{button:3d} down: {down_seconds:4.2f} seconds"
32
+
33
+ return s
34
+
35
+ def print_state_string(self, extra_string=None):
36
+ print(self.state_string(extra_string))
37
+
38
+ def event(self, event, debugging=False):
39
+ if event.type == pygame.JOYBUTTONUP:
40
+ self.button_down_at[event.button] = 0
41
+ elif event.type == pygame.JOYBUTTONDOWN:
42
+ self.button_down_at[event.button] = time.perf_counter()
43
+ elif event.type == pygame.JOYAXISMOTION:
44
+ value = event.value
45
+
46
+ if abs(value) < XboxState.JOYSTICK_DRIFT:
47
+ value = 0.0
48
+
49
+ self.axis_value[event.axis] = round(value, 2)
50
+ else:
51
+ print("Unprocessed event:", event)
52
+
53
+ if debugging:
54
+ print(self.state_string())
55
+
56
+ def left_x(self):
57
+ return self.axis_value[0]
58
+
59
+ def left_y(self):
60
+ return self.axis_value[1]
61
+
62
+ def left_axis(self):
63
+ return self.left_x(), self.left_y()
64
+
65
+ def right_x(self):
66
+ return self.axis_value[2]
67
+
68
+ def right_y(self):
69
+ return self.axis_value[3]
70
+
71
+ def right_axis(self):
72
+ return self.right_x(), self.right_y()
73
+
74
+ def adjusted_trigger_value(self, value):
75
+ return round((value + 1.0) / 2.0, 2)
76
+
77
+ def left_trigger(self):
78
+ return self.adjusted_trigger_value(self.axis_value[4])
79
+
80
+ def right_trigger(self):
81
+ return self.adjusted_trigger_value(self.axis_value[5])
82
+
83
+ def left_trigger_raw(self):
84
+ return self.axis_value[4]
85
+
86
+ def right_trigger_raw(self):
87
+ return self.axis_value[5]
88
+
89
+ def button_down_seconds(self, button):
90
+ if self.button_down_at[button] <= 0:
91
+ return 0
92
+
93
+ return time.perf_counter() - self.button_down_at[button]
94
+
95
+ def button_down_milliseconds(self, button):
96
+ return int(self.button_down_seconds(button) * 1000.0)
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: robot-xbox-controller
3
+ Version: 0.1.0
4
+ Summary: Library for xbox controllers.
5
+ Project-URL: Documentation, https://github.com/fmorton/robot-python-xbox-controller#readme
6
+ Project-URL: Issues, https://github.com/fmorton/robot-python-xbox-controller/issues
7
+ Project-URL: Source, https://github.com/fmorton/robot-python-xbox-controller
8
+ Author-email: Frank Morton <fmorton@mac.com>
9
+ License-Expression: LGPL-3.0-only
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: Implementation :: CPython
19
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: pygame
22
+ Provides-Extra: dev
23
+ Requires-Dist: black; extra == 'dev'
24
+ Requires-Dist: flake8; extra == 'dev'
25
+ Requires-Dist: pylint; extra == 'dev'
26
+ Requires-Dist: pytest; extra == 'dev'
27
+ Requires-Dist: pytest-mock; extra == 'dev'
28
+ Provides-Extra: test
29
+ Requires-Dist: flake8-pyproject; extra == 'test'
30
+ Requires-Dist: pytest-cov; extra == 'test'
31
+ Requires-Dist: pytest-mock; extra == 'test'
32
+ Requires-Dist: pytest>=8.3; extra == 'test'
33
+ Description-Content-Type: text/x-rst
34
+
35
+ ========
36
+ Overview
37
+ ========
38
+
39
+
40
+ .. start-badges
41
+
42
+ .. list-table::
43
+ :stub-columns: 1
44
+
45
+ * - docs
46
+ - |docs|
47
+ * - package
48
+ - | |version| |wheel| |supported-versions|
49
+ .. |docs| image:: https://readthedocs.org/projects/robot-python-xbox-controller/badge/?style=flat
50
+ :target: https://robot-xbox-controller.readthedocs.io/
51
+ :alt: Documentation Status
52
+
53
+ .. |github-actions| image:: https://github.com/fmorton/robot-python-xbox-controller/actions/workflows/github-actions.yml/badge.svg
54
+ :alt: GitHub Actions Build Status
55
+ :target: https://github.com/fmorton/robot-python-xbox-controller/actions
56
+
57
+ .. |codecov| image:: https://codecov.io/gh/fmorton/robot-xbox-controller/branch/main/graphs/badge.svg?branch=main
58
+ :alt: Coverage Status
59
+ :target: https://codecov.io/github/fmorton/robot-xbox-controller
60
+
61
+ .. |version| image:: https://img.shields.io/pypi/v/robot-xbox-controller.svg
62
+ :alt: PyPI Package latest release
63
+ :target: https://pypi.org/project/robot-xbox-controller
64
+
65
+ .. |wheel| image:: https://img.shields.io/pypi/wheel/robot-xbox-controller.svg
66
+ :alt: PyPI Wheel
67
+ :target: https://pypi.org/project/robot-xbox-controller
68
+
69
+ .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/robot-xbox-controller.svg
70
+ :alt: Supported versions
71
+ :target: https://pypi.org/project/robot-xbox-controller
72
+
73
+ .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/robot-xbox-controller.svg
74
+ :alt: Supported implementations
75
+ :target: https://pypi.org/project/robot-xbox-controller
76
+
77
+
78
+ .. end-badges
79
+
80
+ Library for xbox controllers.
81
+
82
+ * Free software: MIT License
83
+
84
+ Installation
85
+ ============
86
+
87
+ ::
88
+
89
+ pip install robot-xbox-controller
90
+
91
+ You can also install the in-development version with::
92
+
93
+ pip install https://github.com/fmorton/robot-xbox-controller/archive/main.zip
94
+
95
+
96
+
97
+ Xbox Controller Example
98
+ =======================
99
+
100
+ .. code-block:: python
101
+
102
+ import pygame
103
+
104
+ from robot.xbox_controller import XboxController
105
+
106
+ controller = XboxController().connect()
107
+
108
+ print("Controller connected")
109
+
110
+ running = True
111
+
112
+ while running:
113
+ for event in pygame.event.get():
114
+ if event.type == pygame.QUIT:
115
+ running = False
116
+ elif event.type == pygame.JOYAXISMOTION:
117
+ print(f"Controller axis {event.axis} value {event.value}")
118
+ elif event.type == pygame.JOYBUTTONDOWN:
119
+ print(f"Button {event.button} pressed on controller {event.joy}")
120
+
121
+ if event.button == XboxController.BUTTON_XBOX:
122
+ running = False
123
+
124
+
125
+ Xbox Controller Example Using run() and the Controller State
126
+ ============================================================
127
+
128
+ The run() loop maintains the state of the controllers and all buttons for processing by your supplied method, "robot" in the example below.
129
+
130
+ .. code-block:: python
131
+
132
+ from robot.xbox_controller import XboxController
133
+ from time import sleep
134
+
135
+
136
+ def robot(controller):
137
+ print("runner", controller.state.state_string())
138
+
139
+ if controller.state.button_down_milliseconds(XboxController.BUTTON_XBOX):
140
+ return False
141
+
142
+ sleep(XboxController.EVENT_LOOP_DELAY)
143
+
144
+ return True
145
+
146
+
147
+ controller = XboxController().connect()
148
+
149
+ controller.run(robot)
150
+
151
+
152
+
153
+ The controller.state has the following methods to retrieve the selections.
154
+
155
+ .. code-block:: python
156
+
157
+ controller.state.left_x() # returns x value of left controller (-1 means max forward/1 max backward)
158
+ controller.state.left_y() # returns y value of left controller (-1 means max left/1 max right)
159
+ controller.state.left_axis() # returns left_x, left_y
160
+
161
+ controller.state.right_x() # returns x value of right controller (-1 means max forward/1 max backward)
162
+ controller.state.right_y() # returns y value of right controller (-1 means max left/1 max right)
163
+ controller.state.right_axis() # returns right_x, right_y
164
+
165
+ controller.state.left_trigger() # returns left trigger value (0 to 1 if fully pressed)
166
+ controller.state.right_trigger() # returns right trigger value (0 to 1 if fully pressed)
167
+
168
+ controller.state.left_trigger_raw() # returns left trigger unadjusted value (-1 to 1 if fully pressed)
169
+ controller.state.right_trigger_raw() # returns right trigger unadjusted value (-1 to 1 if fully pressed)
170
+
171
+ controller.state.button_down_seconds(button) # how many seconds "button" has been pressed
172
+ controller.state.button_down_milliseconds(button) # how many milliseconds "button" has been pressed
173
+
174
+
175
+
176
+ The following button constant values are available in XboxController:
177
+
178
+ .. code-block:: python
179
+
180
+ BUTTON_A = 0
181
+ BUTTON_B = 1
182
+ BUTTON_X = 2
183
+ BUTTON_Y = 3
184
+ BUTTON_VIEW = 4 # left middle button
185
+ BUTTON_XBOX = 5 # center middle button
186
+ BUTTON_SHARE = 6 # right middle button
187
+ BUTTON_LEFT_JOYSTICK = 7
188
+ BUTTON_RIGHT_JOYSTICK = 8
189
+ BUTTON_LEFT_BUMPER = 9
190
+ BUTTON_RIGHT_BUMPER = 10
191
+ BUTTON_FORWARD = 11
192
+ BUTTON_BACKWARD = 12
193
+ BUTTON_LEFT = 13
194
+ BUTTON_RIGHT = 14
195
+
196
+
197
+
198
+ Testing
199
+ =======
200
+
201
+ To run all the tests run::
202
+
203
+ pytest -s
@@ -0,0 +1,6 @@
1
+ robot/xbox_controller.py,sha256=nyQVqwWOKkyhaUNx5bYcUsxHaWBt5jxAqlNOTj-aPe8,3599
2
+ robot/xbox_state.py,sha256=y93Akl2pAMnyO3DtejTKeKW9PyY-Yw1GVgE2pDC2UQE,2764
3
+ robot_xbox_controller-0.1.0.dist-info/METADATA,sha256=BOXdPtrDJPdyccicdzH1cbRpcU3s-J0eYs5n6qSeiMM,6453
4
+ robot_xbox_controller-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ robot_xbox_controller-0.1.0.dist-info/licenses/LICENSE,sha256=jg4vPk0w0vVVIE12vUtvLN-8x8yrxoS5z0_WwirTHGo,1092
6
+ robot_xbox_controller-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frank Morton (fmorton@base2inc.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.