robot-xbox-controller 0.1.0__tar.gz

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,80 @@
1
+ *.py[cod]
2
+ __pycache__
3
+
4
+ # C extensions
5
+ *.so
6
+
7
+ # Packages
8
+ *.egg
9
+ *.egg-info
10
+ dist
11
+ build
12
+ eggs
13
+ .eggs
14
+ parts
15
+ bin
16
+ var
17
+ sdist
18
+ wheelhouse
19
+ develop-eggs
20
+ .installed.cfg
21
+ lib
22
+ lib64
23
+ venv*/
24
+ pyvenv*/
25
+ pip-wheel-metadata/
26
+
27
+ # Installer logs
28
+ pip-log.txt
29
+
30
+ # Unit test / coverage reports
31
+ .coverage
32
+ .tox
33
+ .coverage.*
34
+ .pytest_cache/
35
+ nosetests.xml
36
+ coverage.xml
37
+ htmlcov
38
+
39
+ # Translations
40
+ *.mo
41
+
42
+ # Buildout
43
+ .mr.developer.cfg
44
+
45
+ # IDE project files
46
+ .project
47
+ .pydevproject
48
+ .idea
49
+ .vscode
50
+ *.iml
51
+ *.komodoproject
52
+
53
+ # Complexity
54
+ output/*.html
55
+ output/*/index.html
56
+
57
+ # Sphinx
58
+ docs/_build
59
+
60
+ .DS_Store
61
+ *~
62
+ .*.sw[po]
63
+ .build
64
+ .ve
65
+ .env
66
+ .cache
67
+ .pytest
68
+ .benchmarks
69
+ .bootstrap
70
+ .appveyor.token
71
+ *.bak
72
+
73
+ # Mypy Cache
74
+ .mypy_cache/
75
+
76
+ .cookiecutterrc
77
+ .bumpversion.cfg
78
+ .ruby-version
79
+
80
+ .pypirc
@@ -0,0 +1 @@
1
+ 3.13.2
@@ -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.
@@ -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,169 @@
1
+ ========
2
+ Overview
3
+ ========
4
+
5
+
6
+ .. start-badges
7
+
8
+ .. list-table::
9
+ :stub-columns: 1
10
+
11
+ * - docs
12
+ - |docs|
13
+ * - package
14
+ - | |version| |wheel| |supported-versions|
15
+ .. |docs| image:: https://readthedocs.org/projects/robot-python-xbox-controller/badge/?style=flat
16
+ :target: https://robot-xbox-controller.readthedocs.io/
17
+ :alt: Documentation Status
18
+
19
+ .. |github-actions| image:: https://github.com/fmorton/robot-python-xbox-controller/actions/workflows/github-actions.yml/badge.svg
20
+ :alt: GitHub Actions Build Status
21
+ :target: https://github.com/fmorton/robot-python-xbox-controller/actions
22
+
23
+ .. |codecov| image:: https://codecov.io/gh/fmorton/robot-xbox-controller/branch/main/graphs/badge.svg?branch=main
24
+ :alt: Coverage Status
25
+ :target: https://codecov.io/github/fmorton/robot-xbox-controller
26
+
27
+ .. |version| image:: https://img.shields.io/pypi/v/robot-xbox-controller.svg
28
+ :alt: PyPI Package latest release
29
+ :target: https://pypi.org/project/robot-xbox-controller
30
+
31
+ .. |wheel| image:: https://img.shields.io/pypi/wheel/robot-xbox-controller.svg
32
+ :alt: PyPI Wheel
33
+ :target: https://pypi.org/project/robot-xbox-controller
34
+
35
+ .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/robot-xbox-controller.svg
36
+ :alt: Supported versions
37
+ :target: https://pypi.org/project/robot-xbox-controller
38
+
39
+ .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/robot-xbox-controller.svg
40
+ :alt: Supported implementations
41
+ :target: https://pypi.org/project/robot-xbox-controller
42
+
43
+
44
+ .. end-badges
45
+
46
+ Library for xbox controllers.
47
+
48
+ * Free software: MIT License
49
+
50
+ Installation
51
+ ============
52
+
53
+ ::
54
+
55
+ pip install robot-xbox-controller
56
+
57
+ You can also install the in-development version with::
58
+
59
+ pip install https://github.com/fmorton/robot-xbox-controller/archive/main.zip
60
+
61
+
62
+
63
+ Xbox Controller Example
64
+ =======================
65
+
66
+ .. code-block:: python
67
+
68
+ import pygame
69
+
70
+ from robot.xbox_controller import XboxController
71
+
72
+ controller = XboxController().connect()
73
+
74
+ print("Controller connected")
75
+
76
+ running = True
77
+
78
+ while running:
79
+ for event in pygame.event.get():
80
+ if event.type == pygame.QUIT:
81
+ running = False
82
+ elif event.type == pygame.JOYAXISMOTION:
83
+ print(f"Controller axis {event.axis} value {event.value}")
84
+ elif event.type == pygame.JOYBUTTONDOWN:
85
+ print(f"Button {event.button} pressed on controller {event.joy}")
86
+
87
+ if event.button == XboxController.BUTTON_XBOX:
88
+ running = False
89
+
90
+
91
+ Xbox Controller Example Using run() and the Controller State
92
+ ============================================================
93
+
94
+ The run() loop maintains the state of the controllers and all buttons for processing by your supplied method, "robot" in the example below.
95
+
96
+ .. code-block:: python
97
+
98
+ from robot.xbox_controller import XboxController
99
+ from time import sleep
100
+
101
+
102
+ def robot(controller):
103
+ print("runner", controller.state.state_string())
104
+
105
+ if controller.state.button_down_milliseconds(XboxController.BUTTON_XBOX):
106
+ return False
107
+
108
+ sleep(XboxController.EVENT_LOOP_DELAY)
109
+
110
+ return True
111
+
112
+
113
+ controller = XboxController().connect()
114
+
115
+ controller.run(robot)
116
+
117
+
118
+
119
+ The controller.state has the following methods to retrieve the selections.
120
+
121
+ .. code-block:: python
122
+
123
+ controller.state.left_x() # returns x value of left controller (-1 means max forward/1 max backward)
124
+ controller.state.left_y() # returns y value of left controller (-1 means max left/1 max right)
125
+ controller.state.left_axis() # returns left_x, left_y
126
+
127
+ controller.state.right_x() # returns x value of right controller (-1 means max forward/1 max backward)
128
+ controller.state.right_y() # returns y value of right controller (-1 means max left/1 max right)
129
+ controller.state.right_axis() # returns right_x, right_y
130
+
131
+ controller.state.left_trigger() # returns left trigger value (0 to 1 if fully pressed)
132
+ controller.state.right_trigger() # returns right trigger value (0 to 1 if fully pressed)
133
+
134
+ controller.state.left_trigger_raw() # returns left trigger unadjusted value (-1 to 1 if fully pressed)
135
+ controller.state.right_trigger_raw() # returns right trigger unadjusted value (-1 to 1 if fully pressed)
136
+
137
+ controller.state.button_down_seconds(button) # how many seconds "button" has been pressed
138
+ controller.state.button_down_milliseconds(button) # how many milliseconds "button" has been pressed
139
+
140
+
141
+
142
+ The following button constant values are available in XboxController:
143
+
144
+ .. code-block:: python
145
+
146
+ BUTTON_A = 0
147
+ BUTTON_B = 1
148
+ BUTTON_X = 2
149
+ BUTTON_Y = 3
150
+ BUTTON_VIEW = 4 # left middle button
151
+ BUTTON_XBOX = 5 # center middle button
152
+ BUTTON_SHARE = 6 # right middle button
153
+ BUTTON_LEFT_JOYSTICK = 7
154
+ BUTTON_RIGHT_JOYSTICK = 8
155
+ BUTTON_LEFT_BUMPER = 9
156
+ BUTTON_RIGHT_BUMPER = 10
157
+ BUTTON_FORWARD = 11
158
+ BUTTON_BACKWARD = 12
159
+ BUTTON_LEFT = 13
160
+ BUTTON_RIGHT = 14
161
+
162
+
163
+
164
+ Testing
165
+ =======
166
+
167
+ To run all the tests run::
168
+
169
+ pytest -s
@@ -0,0 +1,22 @@
1
+ import pygame
2
+
3
+ from robot.xbox_controller import XboxController
4
+
5
+
6
+ controller = XboxController().connect()
7
+
8
+ print("Controller connected")
9
+
10
+ running = True
11
+
12
+ while running:
13
+ for event in pygame.event.get():
14
+ if event.type == pygame.QUIT:
15
+ running = False
16
+ elif event.type == pygame.JOYAXISMOTION:
17
+ print(f"Controller axis {event.axis} value {event.value}")
18
+ elif event.type == pygame.JOYBUTTONDOWN:
19
+ print(f"Button {event.button} pressed on controller {event.joy}")
20
+
21
+ if event.button == XboxController.BUTTON_XBOX:
22
+ running = False
@@ -0,0 +1,19 @@
1
+ from robot.xbox_controller import XboxController
2
+ from time import sleep
3
+
4
+
5
+
6
+ def robot(controller):
7
+ print("robot", controller.state.state_string())
8
+
9
+ if controller.state.button_down_milliseconds(XboxController.BUTTON_XBOX):
10
+ return False
11
+
12
+ sleep(XboxController.EVENT_LOOP_DELAY)
13
+
14
+ return True
15
+
16
+
17
+ controller = XboxController().connect()
18
+
19
+ controller.run(robot)
@@ -0,0 +1,79 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+
6
+ [project]
7
+ name = "robot-xbox-controller"
8
+ version = "0.1.0"
9
+ description = "Library for xbox controllers."
10
+ readme = "README.rst"
11
+ requires-python = ">=3.9"
12
+ license = "LGPL-3.0-only"
13
+ keywords = []
14
+ authors = [
15
+ { name = "Frank Morton", email = "fmorton@mac.com" },
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: Implementation :: CPython",
26
+ "Programming Language :: Python :: Implementation :: PyPy",
27
+ ]
28
+ dependencies = [ "pygame" ]
29
+
30
+ [project.urls]
31
+ Documentation = "https://github.com/fmorton/robot-python-xbox-controller#readme"
32
+ Issues = "https://github.com/fmorton/robot-python-xbox-controller/issues"
33
+ Source = "https://github.com/fmorton/robot-python-xbox-controller"
34
+
35
+ [project.optional-dependencies]
36
+ test = [
37
+ "pytest>=8.3",
38
+ "pytest-cov",
39
+ "pytest-mock",
40
+ "flake8-pyproject",
41
+ ]
42
+ dev = [
43
+ "pylint",
44
+ "pytest",
45
+ "pytest-mock",
46
+ "black",
47
+ "flake8",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/robot"]
52
+
53
+ [tool.black]
54
+ line-length = 132
55
+ target-version = ["py37"]
56
+ skip-string-normalization = true
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
60
+
61
+ [tool.pylint.messages_control]
62
+ disable = [
63
+ "line-too-long",
64
+ "missing-class-docstring",
65
+ "missing-function-docstring",
66
+ "missing-module-docstring",
67
+ "redefined-builtin",
68
+ "too-few-public-methods",
69
+ "too-many-arguments",
70
+ "too-many-positional-arguments"
71
+ ]
72
+
73
+ [tool.flake8]
74
+ max-line-length = 132
75
+ exclude = [
76
+ ".git",
77
+ "__pycache__",
78
+ ".venv"
79
+ ]
@@ -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)
@@ -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,93 @@
1
+ import pygame
2
+ import pytest
3
+
4
+ from robot.xbox_controller import XboxController
5
+ from time import sleep
6
+
7
+ controller = XboxController().connect()
8
+
9
+
10
+ def test_xbox_controller():
11
+ print("Controller connected")
12
+
13
+ running = True
14
+
15
+ while running:
16
+ for event in pygame.event.get():
17
+ if controller.quit_event(event):
18
+ running = False
19
+ elif event.type == pygame.JOYBUTTONDOWN:
20
+ print(f"Button {event.button} pressed on controller {event.joy}")
21
+
22
+ if event.button == 15: # options button to quit
23
+ running = False
24
+ elif event.type == pygame.JOYAXISMOTION:
25
+ print(f"Controller axis {event.axis} value {event.value}")
26
+
27
+ assert True
28
+
29
+
30
+ def wheel_speed_calculation(x_axis, y_axis, left_speed_target, right_speed_target, max_speed=1.0):
31
+ left_speed, right_speed = XboxController.wheel_speeds(x_axis, y_axis, max_speed)
32
+
33
+ assert left_speed == left_speed_target
34
+ assert right_speed == right_speed_target
35
+
36
+
37
+ def test_wheel_speeds():
38
+ wheel_speed_calculation(0.0, -1.0, 1.0, 1.0) # forward
39
+ wheel_speed_calculation(0.0, -0.5, 0.5, 0.5) # forward slow
40
+
41
+ wheel_speed_calculation(0.0, 1.0, -1.0, -1.0) # backwards
42
+ wheel_speed_calculation(0.0, 0.5, -0.5, -0.5) # backwards slow
43
+
44
+ wheel_speed_calculation(-1.0, 0.0, -1.0, 1.0) # left
45
+ wheel_speed_calculation(-0.5, 0.0, -0.5, 0.5) # left slow
46
+
47
+ wheel_speed_calculation(1.0, 0.0, 1.0, -1.0) # right
48
+ wheel_speed_calculation(0.5, 0.0, 0.5, -0.5) # right slow
49
+
50
+ wheel_speed_calculation(0.0, -1.0, 1.0, 1.0) # forward
51
+ wheel_speed_calculation(0.0, -0.5, 0.5, 0.5) # forward slow
52
+
53
+ wheel_speed_calculation(1.0, -1.0, 1.0, 0.0) # right 45 degrees
54
+ wheel_speed_calculation(0.5, -0.5, 1.0, 0.0) # right 45 degrees slow
55
+
56
+ wheel_speed_calculation(0.0, -1.0, 100.0, 100.0, 100.0) # forward (100 scale)
57
+ wheel_speed_calculation(0.0, -0.5, 50.0, 50.0, 100.0) # forward slow (100 scale)
58
+
59
+
60
+ def test_wheel_speed_and_angle():
61
+ speed, angle = XboxController.wheel_speed_and_angle(0.0, -1.0)
62
+ assert speed == 1.0
63
+ assert angle == 90.0
64
+
65
+
66
+ def test_state(): # this code is the same as controller.run()
67
+ running = True
68
+
69
+ while running:
70
+ for event in pygame.event.get():
71
+ if controller.quit_event(event):
72
+ running = False
73
+ else:
74
+ controller.state.event(event, True)
75
+
76
+
77
+ def runner(controller):
78
+ controller.state.print_state_string("extra")
79
+
80
+ sleep(XboxController.EVENT_LOOP_DELAY)
81
+
82
+ if controller.state.button_down_milliseconds(XboxController.BUTTON_XBOX):
83
+ return False
84
+
85
+ return True
86
+
87
+
88
+ def test_run():
89
+ controller.run(runner)
90
+
91
+
92
+ def test_tick():
93
+ controller.tick(30)