├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── _static │ ├── css │ │ └── no_breadcrumbs.css │ └── favicon.ico ├── _templates │ ├── breadcrumbs.html │ └── layout.html ├── api.rst ├── conf.py ├── examples.rst ├── index.rst ├── intro.rst ├── make.bat ├── requirements.txt └── start.rst ├── examples ├── 0_boot.py │ ├── minimal │ │ └── boot.py │ └── standard │ │ └── boot.py ├── 1_start_simple │ ├── boot.py │ └── code.py ├── 2_more_inputs │ ├── boot.py │ └── code.py ├── 3_button_operations │ ├── boot.py │ └── code.py ├── 4_gpio_expander │ ├── boot.py │ └── code.py ├── 5_external_adc │ ├── boot.py │ └── code.py ├── 6_capacitive_touch │ ├── boot.py │ └── code.py └── 7_hotas │ ├── stick │ ├── boot.py │ └── code.py │ └── throttle │ └── code.py ├── extras ├── hid_descriptors │ ├── README.md │ ├── buffalo_gamepad.html │ ├── images │ │ ├── buffalo_gamepad.jpg │ │ ├── logitech_x52_pro.jpg │ │ ├── nvidia_shield.jpg │ │ └── sony_ds4.jpg │ ├── logitech_x52_pro.html │ ├── nvidia_shield_controller.html │ └── sony_dualshock_4.html └── scripts │ ├── axis_visualizer.py │ └── bundle_release.py └── joystick_xl ├── __init__.py ├── hid.py ├── inputs.py ├── joystick.py └── tools.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Prevent HTML files from being detected as the project language. 2 | *.html linguist-vendored -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fasteddy516] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # CircuitPython/MicroPython pre-compiled code 132 | *.mpy 133 | 134 | # Visual Studio Code 135 | .vscode/ 136 | *.code-workspace 137 | .history/ 138 | 139 | # Release bundles 140 | *.zip 141 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # File: .readthedocs.yaml 2 | 3 | # Required 4 | version: 2 5 | 6 | # Set the OS, Python version and other tools you might need 7 | build: 8 | os: "ubuntu-22.04" 9 | tools: 10 | python: "3.12" 11 | 12 | # Build from the docs/ directory with Sphinx 13 | sphinx: 14 | configuration: docs/conf.py 15 | 16 | # Declare the Python requirements to build documentation 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Edward Wright 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. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | JoystickXL for CircuitPython 2 | ============================ 3 | .. image:: https://img.shields.io/github/license/fasteddy516/CircuitPython_JoystickXL 4 | :target: https://github.com/fasteddy516/CircuitPython_JoystickXL/blob/master/LICENSE 5 | :alt: License 6 | 7 | .. image:: https://img.shields.io/badge/code%20style-black-000000 8 | :target: https://github.com/psf/black 9 | :alt: Black 10 | 11 | .. image:: https://readthedocs.org/projects/circuitpython-joystickxl/badge/?version=latest 12 | :target: https://circuitpython-joystickxl.readthedocs.io/en/latest/?badge=latest 13 | :alt: Documentation Status 14 | 15 | .. image:: https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Open%20in%20Visual%20Studio%20Code&labelColor=2c2c32&color=007acc&logoColor=007acc 16 | :target: https://open.vscode.dev/fasteddy516/CircuitPython_JoystickXL 17 | :alt: Open in Visual Studio Code 18 | 19 | 20 | Description 21 | =========== 22 | This CircuitPython driver simulates a *really big* USB HID joystick device - up 23 | to 8 axes, 128 buttons and 4 hat (POV) switches. If you want to build a custom 24 | game controller with a lot of inputs - *I'm looking at you, space/flight sim 25 | pilots, racing sim drivers and virtual farmers* - JoystickXL can help. 26 | 27 | 28 | Requirements 29 | ============ 30 | *This driver relies on features that were introduced in CircuitPython 31 | version 7.x* **You must be running CircuitPython 7.0.0 or newer 32 | on your device in order to use JoystickXL.** 33 | 34 | * This driver was made for devices running `Adafruit CircuitPython `_. 35 | For a list of compatible devices, see `circuitpython.org `_. 36 | 37 | * There are no dependencies on any other CircuitPython drivers, libraries or modules. 38 | 39 | * Pre-compiled (``.mpy``) versions of JoystickXL are available in the `releases `_ 40 | section for CircuitPython versions 8.x and 9.x. 41 | 42 | 43 | Limitations 44 | =========== 45 | * A wired USB connection to the host device is required. *Bluetooth 46 | connectivity is not supported at this time.* 47 | 48 | * Axis data is reported with 8-bit resolution (values ranging from 0-255). 49 | 50 | * Only one JoystickXL device can be defined per CircuitPython board. *You 51 | cannot have a single board report as two or more independant joysticks.* 52 | 53 | * JoystickXL's reporting frequency - thus, input latency - is affected by 54 | many factors, including processor speed, the number of inputs that need 55 | to be processed, and the latency of any external input peripherals that 56 | are being used. The reporting frequency is going to be significantly 57 | higher on a Metro M4 Express using on-board GPIO than it is on a QT-PY 58 | using I2C/SPI-based I/O expanders. 59 | 60 | 61 | Host OS/Software Compatibility 62 | ============================== 63 | On **Windows 10/11**, all 8 axes 128 buttons and 4 hat switches are supported at 64 | the operating system level, and JoystickXL has been tested and confirmed to work 65 | with the following games: 66 | 67 | * **Microsoft Flight Simulator (2020)** *(All inputs)* 68 | * **Elite Dangerous** *(Limited to 32 buttons)* 69 | * **Star Citizen** *(All inputs)* 70 | * **Digital Combat Simulator (DCS) World** *(All inputs)* 71 | * **EverSpace 2** *(All inputs - hat switches are considered to be axes)* 72 | * **Forza Horizon 4** *(All inputs)* 73 | * **BeamNG.drive** *(Limited to 7 axes and 1 hat switch)* 74 | * **Farming Simulator 19** *(Limited to 7 axes, 24 buttons and 1 hat switch)* 75 | 76 | *Note that any game-specific input limitations mentioned above are - to the 77 | best of my knowledge - a result of the game's joystick implementation, and are 78 | not unique to JoystickXL.* 79 | 80 | On **Linux**, a very limited amount of testing has been done on a Raspberry Pi 81 | 4B using ``jstest`` (part of the ``joystick`` package). The first 7 axes and 82 | 80 buttons work correctly. Axis 8 does not register any events, nor do any 83 | buttons beyond the first 80. Only a single hat switch *sort of* works, but it 84 | gets interpreted as two axes rather than an actual hat switch. Other Linux 85 | platforms/distributions/applications have not been tested. 86 | 87 | No testing has been done on an **Apple/Mac** platform. 88 | 89 | 90 | Documentation 91 | ============= 92 | Full documentation is available at ``_. 93 | 94 | 95 | Installation 96 | ============ 97 | 1. Download the `latest release of JoystickXL `_ 98 | that corresponds to the version of CircuitPython you're running. (i.e. 99 | ``joystick_xl_x.x.x_cp8`` for CircuitPython 8.x) 100 | 2. Extract the files from the downloaded .zip archive. 101 | 3. Copy the ``joystick_xl`` folder to the ``lib`` folder on your device's 102 | ``CIRCUITPY`` drive. 103 | 104 | For additional information on installing libraries, see Adafruit's 105 | `Welcome to CircuitPython Guide `_. 106 | 107 | 108 | Using JoystickXL 109 | ================ 110 | 1. Create/modify ``boot.py`` on your CircuitPython device to enable the 111 | required custom USB HID device. 112 | 113 | .. code:: python 114 | 115 | """boot.py""" 116 | import usb_hid 117 | from joystick_xl.hid import create_joystick 118 | 119 | # enable default CircuitPython USB HID devices as well as JoystickXL 120 | usb_hid.enable( 121 | ( 122 | usb_hid.Device.KEYBOARD, 123 | usb_hid.Device.MOUSE, 124 | usb_hid.Device.CONSUMER_CONTROL, 125 | create_joystick(axes=2, buttons=2, hats=1), 126 | ) 127 | ) 128 | 129 | 2. Use JoystickXL in ``code.py`` like this: 130 | 131 | .. code:: python 132 | 133 | """code.py""" 134 | import board 135 | from joystick_xl.inputs import Axis, Button, Hat 136 | from joystick_xl.joystick import Joystick 137 | 138 | js = Joystick() 139 | 140 | js.add_input( 141 | Button(board.D9), 142 | Button(board.D10), 143 | Axis(board.A2), 144 | Axis(board.A3), 145 | Hat(up=board.D2, down=board.D3, left=board.D4, right=board.D7), 146 | ) 147 | 148 | while True: 149 | js.update() 150 | 151 | See the `examples `_ 152 | and `API documentation `_ 153 | for more information. 154 | 155 | 156 | Testing JoystickXL Devices 157 | ========================== 158 | Not all platforms/games/applications support joystick devices with high input 159 | counts. **Before you spend any time writing code or building hardware for a 160 | custom controller, you should make sure the software that you want to use it 161 | with is compatible.** 162 | 163 | Fortunately, JoystickXL has a built-in testing module that can be run right 164 | from the CircuitPython Serial Console/REPL to verify compatibility with an 165 | operating system, game or application - *no input wiring or code.py required!* 166 | 167 | See the 168 | `compatibility and testing documentation `_ 169 | for more information. 170 | 171 | 172 | Contributing 173 | ============ 174 | If you have questions, problems, feature requests, etc. please post them to the 175 | `Issues section on Github `_. 176 | If you would like to contribute, please let me know. 177 | 178 | 179 | Acknowledgements 180 | ============================ 181 | A massive thanks to Adafruit and the entire CircuitPython team for creating and 182 | constantly improving the CircuitPython ecosystem. 183 | 184 | Frank Zhao's 185 | `Tutorial about USB HID Report Descriptors `_ 186 | was the starting point for my journey into USB HID land. 187 | 188 | The tools and documentation provided by the `USB Implementors Forum `_ 189 | were an excellent resource, especially in regards to the creation of the 190 | required USB HID descriptor. The following resources were particularly useful: 191 | 192 | * `HID Descriptor Tool `_ 193 | * `Device Class Definition for HID `_ 194 | * `HID Usage Tables `_ 195 | 196 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/css/no_breadcrumbs.css: -------------------------------------------------------------------------------- 1 | /* Hide "Breadcrumbs" header */ 2 | div[aria-label="breadcrumbs navigation"] { 3 | display: none; 4 | } -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasteddy516/CircuitPython_JoystickXL/9be137f80a1a268e47e4620e1310cf1bdbe5c45a/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_templates/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {%- extends "sphinx_rtd_theme/breadcrumbs.html" %} 2 | 3 | {% block breadcrumbs %} 4 | {% endblock %} 5 | {% block breadcrumbs_aside %} 6 | {% endblock %} -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block menu %} 4 | {{ super() }} 5 |

Index

6 | 10 | {% endblock %} -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ``joytsick_xl.hid`` 2 | =============================== 3 | .. automodule:: joystick_xl.hid 4 | :members: 5 | 6 | ``joystick_xl.joystick`` 7 | ========================================= 8 | .. automodule:: joystick_xl.joystick 9 | :members: 10 | 11 | ``joystick_xl.inputs`` 12 | ======================================= 13 | .. automodule:: joystick_xl.inputs 14 | :members: 15 | 16 | ``joystick_xl.tools`` 17 | ======================================= 18 | .. automodule:: joystick_xl.tools 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration file for the Sphinx documentation builder. 3 | 4 | This file only contains a selection of the most common options. For a full 5 | list see the documentation: 6 | https://www.sphinx-doc.org/en/master/usage/configuration.html 7 | """ 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath("..")) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "JoystickXL for CircuitPython" 24 | copyright = "2024 Edward Wright" 25 | author = "Edward Wright" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.viewcode", 36 | ] 37 | 38 | # Ignore imports of these modules, which sphinx will not know about. 39 | autodoc_mock_imports = [ 40 | "analogio", 41 | "board", 42 | "digitalio", 43 | "microcontroller", 44 | "usb_hid", 45 | "supervisor", 46 | ] 47 | autodoc_typehints = "both" 48 | autodoc_member_order = "bysource" 49 | autoclass_content = "init" 50 | 51 | # The default language to highlight source code in. 52 | highlight_language = "python3" 53 | 54 | # If true, '()' will be appended to :func: etc. cross-reference text. 55 | add_function_parentheses = True 56 | 57 | # If true, modules names will be prepended to :func: 58 | add_module_names = False 59 | 60 | # The name of the Pygments (syntax highlighting) style to use. 61 | pygments_style = "sphinx" 62 | 63 | # Add any paths that contain templates here, relative to this directory. 64 | templates_path = ["_templates"] 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path. 69 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 70 | 71 | 72 | # -- Options for HTML output ------------------------------------------------- 73 | 74 | # The theme to use for HTML and HTML Help pages. See the documentation for 75 | # a list of builtin themes. 76 | html_theme = "sphinx_rtd_theme" 77 | 78 | # Add any paths that contain custom static files (such as style sheets) here, 79 | # relative to this directory. They are copied after the builtin static files, 80 | # so a file named "default.css" will overwrite the builtin "default.css". 81 | html_static_path = ["_static"] 82 | 83 | html_favicon = "_static/favicon.ico" 84 | 85 | html_css_files = ["css/no_breadcrumbs.css"] 86 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | 1. Start Simple 2 | =============== 3 | 4 | This is a fully functional joystick with 2 axes, 2 buttons and a single hat 5 | switch. 6 | 7 | .. literalinclude:: ../examples/1_start_simple/code.py 8 | 9 | 10 | 2. More Inputs! 11 | =============== 12 | 13 | This is a fully functional joystick with 8 axes, 24 buttons and 4 hat switches. 14 | Notice the only difference between this example and the *Start Simple* example 15 | is the number of inputs added with ``add_input``. 16 | 17 | .. literalinclude:: ../examples/2_more_inputs/code.py 18 | 19 | 20 | 3. Button Operations 21 | ==================== 22 | 23 | This example shows some of the options available for detecting, processing and 24 | bypassing button presses, which can be useful when you want to start adding 25 | things like LEDs and other sensors to your custom controller. 26 | 27 | .. literalinclude:: ../examples/3_button_operations/code.py 28 | 29 | 30 | 4. GPIO Expander 31 | ================ 32 | 33 | If you find yourself running out of GPIO pins on your CircuitPython board, you 34 | can add I/O expander peripherals to get the extra pins you need. The Microchip 35 | MCP23017 is ideal, as Adafruit has a CircuitPython driver for it that lets us 36 | treat the inputs *almost* exactly like on-board pins. 37 | 38 | Check out Adafruit's `MCP23017 CircuitPython Guide `_ 39 | for more information on how to use this peripheral device. 40 | 41 | .. literalinclude:: ../examples/4_gpio_expander/code.py 42 | 43 | 44 | 5. External ADC 45 | =============== 46 | 47 | Similar to the previous example, this one shows how to use an external 48 | analog-to-digital convertor (Microchip MCP3008) to get additional inputs for 49 | axes. 50 | 51 | Check out Adafruit's `MCP3008 CircuitPython Guide `_ 52 | for more information on how to use this peripheral device. 53 | 54 | .. literalinclude:: ../examples/5_external_adc/code.py 55 | 56 | 57 | 6. Capacitive Touch 58 | =================== 59 | 60 | Adding capacitive touch inputs is simple when you use a device with an existing 61 | CircuitPython driver, such as the Adafruit MPR121 Capacitive Touch Breakout. 62 | 63 | Check out Adafruit's `MPR121 Breakout Guide `_ 64 | for more information on how to use this peripheral device. 65 | 66 | .. literalinclude:: ../examples/6_capacitive_touch/code.py 67 | 68 | 69 | 7. Multi-Unit HOTAS 70 | =================== 71 | 72 | This is a much more complicated example that uses a pair of Adafruit Grand 73 | Central M4 Express boards to create a HOTAS. (If you have no idea what that 74 | is, check out the `Thrustmaster Warthog `_ 75 | or `Logitech X56 `_.) 76 | 77 | The HOTAS example consists of two physically separate units - the *Throttle* 78 | and the *Stick*. Each component could have its own USB connection to the host 79 | computer such that the pair appear as two independant USB HID devices, but this 80 | can get complicated because: 81 | 82 | 1. CircuitPython USB HID game controllers (joystick/gamepad) devices all 83 | identify themselves to the operating system as ``CircuitPython HID``, which 84 | makes it difficult to determine which device is which when more than one 85 | device is connected. 86 | 87 | 2. A number of games/flight sims/racing sims make it difficult to distinguish 88 | between multiple controllers, which makes it challenging to get those 89 | controls configured properly and consistently. 90 | 91 | To alleviate these issues, this example uses a wired (UART) connection 92 | between the Throttle and Stick, and a single USB connection from the Stick 93 | to the host computer. Each piece has 16 buttons, 4 axes and 2 hat switches, 94 | but the whole collection appears to the host computer as a single 32 button, 95 | 8 axis, 4 hat switch joystick. 96 | 97 | This example makes use of JoystickXL's virtual inputs, which allow raw input 98 | values to be assigned to them in code rather then read directly from GPIO pins. 99 | 100 | If you look closely, you'll notice that the only really *complicated* parts 101 | of this example are the bits that deal with the serial communications and the 102 | associated data processing. Everything else is almost identical to the 103 | *Start Simple* example above - create a JoystickXL object, associate inputs with it 104 | and make sure you call the joystick's ``update()`` method in your main loop. 105 | 106 | .. literalinclude:: ../examples/7_hotas/stick/code.py 107 | .. literalinclude:: ../examples/7_hotas/throttle/code.py 108 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :caption: Introduction 3 | :hidden: 4 | 5 | intro 6 | 7 | .. toctree:: 8 | :caption: Getting Started 9 | :hidden: 10 | 11 | start 12 | 13 | .. toctree:: 14 | :caption: Examples 15 | :hidden: 16 | 17 | examples 18 | 19 | .. toctree:: 20 | :caption: API Reference 21 | :maxdepth: 4 22 | :hidden: 23 | 24 | api 25 | 26 | .. toctree:: 27 | :caption: Code On GitHub 28 | :hidden: 29 | 30 | Source 31 | Latest Release 32 | 33 | .. include:: ./intro.rst -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | JoystickXL for CircuitPython 2 | ============================ 3 | .. image:: https://img.shields.io/github/license/fasteddy516/CircuitPython_JoystickXL 4 | :target: https://github.com/fasteddy516/CircuitPython_JoystickXL/blob/master/LICENSE 5 | :alt: License 6 | 7 | .. image:: https://img.shields.io/badge/code%20style-black-000000 8 | :target: https://github.com/psf/black 9 | :alt: Black 10 | 11 | .. image:: https://readthedocs.org/projects/circuitpython-joystickxl/badge/?version=latest 12 | :target: https://circuitpython-joystickxl.readthedocs.io/en/latest/?badge=latest 13 | :alt: Documentation Status 14 | 15 | .. image:: https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Open%20in%20Visual%20Studio%20Code&labelColor=2c2c32&color=007acc&logoColor=007acc 16 | :target: https://open.vscode.dev/fasteddy516/CircuitPython_JoystickXL 17 | :alt: Open in Visual Studio Code 18 | 19 | 20 | Description 21 | =========== 22 | This CircuitPython driver simulates a *really big* USB HID joystick device - up 23 | to 8 axes, 128 buttons and 4 hat (POV) switches. If you want to build a custom 24 | game controller with a lot of inputs - *I'm looking at you, space/flight sim 25 | pilots, racing sim drivers and virtual farmers* - JoystickXL can help. 26 | 27 | **Head over to the** :doc:`Getting Started ` **section to dive in!** 28 | 29 | 30 | Requirements 31 | ============ 32 | *This driver relies on features that were introduced in CircuitPython 33 | version 7.x* **You must be running CircuitPython 7.0.0 or newer 34 | on your device in order to use JoystickXL.** 35 | 36 | * This driver was made for devices running `Adafruit CircuitPython `_. 37 | For a list of compatible devices, see `circuitpython.org `_. 38 | 39 | * There are no dependencies on any other CircuitPython drivers, libraries or modules. 40 | 41 | * Pre-compiled (``.mpy``) versions of JoystickXL are available in the `releases `_ 42 | section for CircuitPython versions 8.x and 9.x. 43 | 44 | 45 | 46 | Limitations 47 | =========== 48 | * A wired USB connection to the host device is required. *Bluetooth 49 | connectivity is not supported at this time.* 50 | 51 | * Axis data is reported with 8-bit resolution (values ranging from 0-255). 52 | 53 | * Only one JoystickXL device can be defined per CircuitPython board. *You 54 | cannot have a single board report as two or more independant joysticks.* 55 | 56 | * JoystickXL's reporting frequency - thus, input latency - is affected by 57 | many factors, including processor speed, the number of inputs that need 58 | to be processed, and the latency of any external input peripherals that 59 | are being used. The reporting frequency is going to be significantly 60 | higher on a Metro M4 Express using on-board GPIO than it is on a QT-PY 61 | using I2C/SPI-based I/O expanders. 62 | 63 | 64 | Host OS/Software Compatibility 65 | ============================== 66 | On **Windows 10**, all 8 axes 128 buttons and 4 hat switches are supported at 67 | the operating system level, and JoystickXL has been tested and confirmed to work 68 | with the following games: 69 | 70 | * **Microsoft Flight Simulator (2020)** *(All inputs)* 71 | * **Elite Dangerous** *(Limited to 32 buttons)* 72 | * **Star Citizen** *(All inputs)* 73 | * **Digital Combat Simulator (DCS) World** *(All inputs)* 74 | * **EverSpace 2** *(All inputs - hat switches are considered to be axes)* 75 | * **Forza Horizon 4** *(All inputs)* 76 | * **BeamNG.drive** *(Limited to 7 axes and 1 hat switch)* 77 | * **Farming Simulator 19** *(Limited to 7 axes, 24 buttons and 1 hat switch)* 78 | 79 | *Note that any game-specific input limitations mentioned above are - to the 80 | best of my knowledge - a result of the game's joystick implementation, and are 81 | not unique to JoystickXL.* 82 | 83 | On **Linux**, a very limited amount of testing has been done on a Raspberry Pi 84 | 4B using ``jstest`` (part of the ``joystick`` package). The first 7 axes and 85 | 80 buttons work correctly. Axis 8 does not register any events, nor do any 86 | buttons beyond the first 80. Only a single hat switch *sort of* works, but it 87 | gets interpreted as two axes rather than an actual hat switch. Other Linux 88 | platforms/distributions/applications have not been tested. 89 | 90 | No testing has been done on an **Apple/Mac** platform. 91 | 92 | 93 | Contributing 94 | ============ 95 | If you have questions, problems, feature requests, etc. please post them to the 96 | `Issues section on Github `_. 97 | If you would like to contribute, please let me know. 98 | 99 | 100 | Acknowledgements 101 | ============================ 102 | A massive thanks to Adafruit and the entire CircuitPython team for creating and 103 | constantly improving the CircuitPython ecosystem. 104 | 105 | The tools and documentation provided by the `USB Implementors Forum `_ 106 | were an excellent resource, especially in regards to the creation of the 107 | required USB HID descriptor. The following resources were particularly useful: 108 | 109 | * `HID Descriptor Tool `_ 110 | * `Device Class Definition for HID `_ 111 | * `HID Usage Tables `_ 112 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # File: docs/requirements.txt 2 | 3 | alabaster==0.7.16 4 | babel==2.14.0 5 | certifi==2024.7.4 6 | charset-normalizer==3.3.2 7 | colorama==0.4.6 8 | docutils==0.20.1 9 | idna==3.7 10 | imagesize==1.4.1 11 | jinja2==3.1.6 12 | markupsafe==2.1.5 13 | packaging==23.2 14 | pygments==2.17.2 15 | readthedocs-sphinx-search==0.3.2 16 | requests==2.32.0 17 | snowballstemmer==2.2.0 18 | sphinx==7.2.6 19 | sphinx-rtd-theme==2.0.0 20 | sphinxcontrib-applehelp==1.0.8 21 | sphinxcontrib-devhelp==1.0.6 22 | sphinxcontrib-htmlhelp==2.0.5 23 | sphinxcontrib-jquery==4.1 24 | sphinxcontrib-jsmath==1.0.1 25 | sphinxcontrib-qthelp==1.0.7 26 | sphinxcontrib-serializinghtml==1.1.10 27 | urllib3==2.2.2 28 | -------------------------------------------------------------------------------- /docs/start.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 1. Download the `latest release of JoystickXL `_ 4 | that corresponds to the version of CircuitPython you're running. (i.e. 5 | ``joystick_xl_x.x.x_cp8`` for CircuitPython 8.x) 6 | 2. Extract the files from the downloaded .zip archive. 7 | 3. Copy the ``joystick_xl`` folder to the ``lib`` folder on your device's 8 | ``CIRCUITPY`` drive. 9 | 10 | .. seealso:: 11 | 12 | For additional information on installing libraries, see Adafruit's 13 | `Welcome to CircuitPython Guide `_. 14 | 15 | 16 | USB HID Configuration 17 | ===================== 18 | In order to use JoystickXL you have to initialize a custom USB HID device in 19 | the ``boot.py`` file on your CircuitPython board. 20 | 21 | If ``boot.py`` does not currently exist in the root folder on your board's 22 | ``CIRCUITPY`` drive, you can create it using the standard example below: 23 | 24 | .. literalinclude:: ../examples/0_boot.py/standard/boot.py 25 | 26 | This enables JoystickXL along with CircuitPython's other standard USB HID 27 | devices. The ``axes``, ``buttons`` and ``hats`` parameters are all set to 28 | maximum values here, but can be lowered if you know you're going to be 29 | using fewer inputs. 30 | 31 | Alternatively, if you're not using any other CircuitPython USB-HID devices 32 | and don't want them to appear on the host, you can enable the joystick 33 | device by itself as shown below. (The ``usb_hid.enable()`` function always 34 | expects a tuple - even when it is only given a single device.) 35 | 36 | .. literalinclude:: ../examples/0_boot.py/minimal/boot.py 37 | 38 | .. note:: 39 | 40 | Once you're finished creating or modifying ``boot.py`` you will have to 41 | hard reset your CircuitPython board by pressing its reset button or 42 | disconnecting it from power for the changes to take effect. 43 | 44 | Once you have ``boot.py`` set up correctly (*and have hard rest your board!*), 45 | you're ready to start working with JoystickXL! Before you dive into hardware 46 | design and coding, though, you should read over the section on 47 | `Verifying Compatibility`_ below. 48 | 49 | .. seealso:: 50 | 51 | For more information about customizing USB devices in CircuitPython, you 52 | can refer to `this excellent guide `_ 53 | on Adafruit's learning system. 54 | 55 | 56 | Verifying Compatibility 57 | ======================= 58 | Not all platforms/games/applications support joystick devices with high input 59 | counts. **Before you spend any time writing code or building hardware for a 60 | custom controller, you should make sure the software that you want to use it 61 | with is compatible.** 62 | 63 | Fortunately, JoystickXL has a built-in testing module that can be run right 64 | from the CircuitPython Serial Console/REPL to verify compatibility with an 65 | operating system, game or application - *no input wiring or code.py required!* 66 | 67 | .. seealso:: 68 | 69 | See Adafruit's `Connecting to the Serial Console `_ 70 | and `The REPL `_ 71 | guides for more information about connecting to - and interacting with - 72 | your board. 73 | 74 | Assuming you 75 | have set up ``boot.py`` as described in the `USB HID Configuration`_ section 76 | above, just enter the following two commands at the ``>>>`` prompt in the 77 | CircuitPython REPL to fire up the JoystickXL test console: 78 | 79 | .. code-block:: text 80 | 81 | Adafruit CircuitPython 7.0.0-alpha.5 on 2021-07-21; Adafruit Trinket M0 with samd21e18 82 | >>> from joystick_xl.tools import TestConsole 83 | >>> TestConsole() 84 | 85 | When the test console loads up, you will be greeted with the following: 86 | 87 | .. code-block:: text 88 | 89 | JoystickXL - Test Console 90 | 91 | Using 1-based indexing. 92 | Button Clicks = 0.25s 93 | Test Button = board.D2 94 | Enter command (? for list) 95 | : 96 | 97 | From here, you can manually activate any axis, button or hat switch and see the 98 | results on the host device. To see a list of available commands, type ``?`` at 99 | the prompt and press enter. The available commands are: 100 | 101 | * ``a`` : **Axis commands** 102 | 103 | * ``[i]u`` : Move axis ``i`` from idle to its maximum value and back. (ex. ``a1u``) 104 | * ``[i]d`` : Move axis ``i`` from idle to its minimum value and back. (ex. ``a4d``) 105 | * ``t`` : Test all configured axes by simulating movement on them one at a time. (ex. ``at``) 106 | 107 | * ``b`` : **Button commands** 108 | 109 | * ``[i]`` : Click button ``i``. (ex. ``b12``) 110 | * ``t`` : Test all configured buttons by simulating clicking them one at a time. (ex. ``bt``) 111 | 112 | * ``h`` : **Hat Commands** 113 | 114 | * ``[i]u`` : Click hat switch ``i``'s ``UP`` button. (ex. ``h1u``) 115 | * ``[i]d`` : Click hat switch ``i``'s ``DOWN`` button. (ex. ``h3d``) 116 | * ``[i]l`` : Click hat switch ``i``'s ``LEFT`` button. (ex. ``h0l``) 117 | * ``[i]r`` : Click hat switch ``i``'s ``RIGHT`` button. (ex. ``h2r``) 118 | * ``[i]ul`` : Click hat switch ``i``'s ``UP+LEFT`` button. (ex. ``h1ul``) 119 | * ``[i]ur`` : Click hat switch ``i``'s ``UP+RIGHT`` button. (ex. ``h3ur``) 120 | * ``[i]dl`` : Click hat switch ``i``'s ``DOWN+LEFT`` button. (ex. ``h0dl``) 121 | * ``[i]dr`` : Click hat switch ``i``'s ``DOWN+RIGHT`` button. (ex. ``h2dr``) 122 | * ``t`` : Test all configured hat switches by clicking each position one at a time. (ex. ``ht``) 123 | 124 | * ``t`` = Test all configured axes, buttons and hats by cycling through their states one at a time. 125 | * ``0`` = Switch to 0-based indexing 126 | * ``1`` = Switch to 1-based indexing 127 | * ``p[t]`` = Set button press time. (ex. ``p150`` = 1.5 second button presses) 128 | * ``q`` = Quit the test console 129 | 130 | .. note:: 131 | 132 | By default, the test console uses **1-based indexing**, which means that 133 | the first axis is ``1``, first button is ``1``, and so on. If your host 134 | device or test application uses **0-based indexing** (the first input is 135 | ``0``), you can switch the test console to use the same numbering scheme 136 | with the ``0`` and ``1`` commands. 137 | 138 | **If you're using a joystick test application** (one that shows all of the 139 | available inputs and their current states), you can use the ``t`` command 140 | to automatically cycle through all available inputs and make sure they register 141 | in the test app. You can also test individual input types with their 142 | corresponding test commands, ``at``, ``bt`` and ``ht``. 143 | 144 | **To test compatibility with a particular game** you should be able to go to 145 | that game's input configuration settings, select your JoystickXL device 146 | (likely labelled ``CircuitPython HID``) and attempt to assign inputs to 147 | functions. Ideally, the game uses a *click to assign* system where you 148 | select the desired function, then move/click the input you want to assign to 149 | it. If so, you can use the corresponding test console command (ex. ``a2u``, 150 | ``b7``, ``h3d``, etc.) to trigger the desired input and make sure it registers 151 | in-game. 152 | 153 | .. warning:: 154 | 155 | Make sure you start the JoystickXL test console before you start the 156 | application you want to test it with on your host. If you start the 157 | application on the host first, it may not detect the joystick. 158 | 159 | **If the application you are trying to test has to be in-focus to capture 160 | joystick events** it will not capture events generated from the test console 161 | because your serial terminal will be in-focus while you are typing in it. 162 | For cases like these, the test console provides a single digital input - by 163 | default on pin ``D2`` (``GP2`` on RP2040-based devices) - which will repeat 164 | the last typed command when activated. You can either hook up an actual 165 | button, or just short the pin to ground to trigger commands as needed. In 166 | the game example above, you would enter the desired command in the test 167 | console and press enter, then switch to the game and use the button input to 168 | trigger that command while the game is in focus. If needed, the button pin 169 | can also be changed when the test console is started as follows: 170 | 171 | .. code-block:: text 172 | 173 | Adafruit CircuitPython 7.0.0-alpha.5 on 2021-07-21; Adafruit Trinket M0 with samd21e18 174 | >>> import board 175 | >>> from joystick_xl.tools import TestConsole 176 | >>> TestConsole(button_pin = board.D7) 177 | 178 | .. seealso:: 179 | 180 | **Joystick Testing Applications** 181 | 182 | * *(Windows)* `Pointy's Joystick Test Application `_ (requires the `Microsoft DirectX End-User Runtimes `_) 183 | * *(Browser-based)* `gamepad-tester.com `_ (Works with up to 6 axes, 32 buttons and 1 hat switch.) 184 | * *(Linux)* `jstest `_ (Part of the ``joystick`` package - works with up to 7 axes, 80 buttons, no hat switches) 185 | 186 | What Next? 187 | ========== 188 | 189 | With your configuration in ``boot.py`` complete, and compatibility with your 190 | desired host application confirmed, you're ready to start building and coding! 191 | 192 | The *building* part is up to your skills and imagination (and beyond the scope 193 | of this documentation). Adafruit has some excellent CircuitPython-specific 194 | guides that can help with the wiring part: 195 | 196 | * For Buttons/Hat Switches - `CircuitPython Digital In & Out `_ 197 | * For Axes - `CircuitPython Analog In `_ 198 | 199 | (Don't worry so much about the code parts in those guides - JoystickXL handles 200 | configuration and processing for standard analog/digital inputs for you - 201 | although it doesn't hurt to know what's going on under the hood!) 202 | 203 | Adafruit also carries some excellent 204 | `button `_, 205 | `switch `_, 206 | `rocker `_, 207 | and `axis `_ 208 | hardware in their store. 209 | 210 | The *coding* part is where JoystickXL comes in. Check out the 211 | :doc:`Examples ` section to see how it's done. Reading through 212 | the first couple of examples should give you a pretty good sense of 213 | how to get started. If your custom controller has no more than 24 buttons, 214 | you may be able to use the *More Inputs* example as-is! 215 | 216 | If you need to dig deeper into JoystickXL's inner workings, check out the 217 | :doc:`API documentation `. 218 | 219 | **Good luck, have fun, and happy coding!** 220 | -------------------------------------------------------------------------------- /examples/0_boot.py/minimal/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL minimal boot.py.""" 2 | 3 | import usb_hid # type: ignore (this is a CircuitPython built-in) 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable a joystick USB HID device. All other standard CircuitPython USB HID 7 | # devices (keyboard, mouse, consumer control) will be disabled. 8 | usb_hid.enable((create_joystick(axes=8, buttons=128, hats=4),)) 9 | -------------------------------------------------------------------------------- /examples/0_boot.py/standard/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore (this is a CircuitPython built-in) 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=8, buttons=128, hats=4), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/1_start_simple/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore (this is a CircuitPython built-in) 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=2, buttons=2, hats=1), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/1_start_simple/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #1 - Start Simple (2 axes, 2 buttons, 1 hat switch). 3 | 4 | Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython 5 | boards with a sufficient quantity/type of pins. 6 | 7 | * Buttons are on pins D9 and D10 8 | * Axes are on pins A2 and A3 9 | * Hat switch is on pins D2 (up), D3 (down), D4 (left) and D7 (right) 10 | 11 | Don't forget to copy boot.py from the example folder to your CIRCUITPY drive. 12 | """ 13 | 14 | import board # type: ignore (this is a CircuitPython built-in) 15 | from joystick_xl.inputs import Axis, Button, Hat 16 | from joystick_xl.joystick import Joystick 17 | 18 | joystick = Joystick() 19 | 20 | joystick.add_input( 21 | Button(board.D9), 22 | Button(board.D10), 23 | Axis(board.A2), 24 | Axis(board.A3), 25 | Hat(up=board.D2, down=board.D3, left=board.D4, right=board.D7), 26 | ) 27 | 28 | while True: 29 | joystick.update() 30 | -------------------------------------------------------------------------------- /examples/2_more_inputs/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore (this is a CircuitPython built-in) 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=8, buttons=24, hats=4), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/2_more_inputs/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #2 - More Inputs! (8 axes, 24 buttons, 4 hat switches). 3 | 4 | Tested on an Adafruit Grand Central M4 Express, but should work on other CircuitPython 5 | boards with a sufficient quantity/type of pins. 6 | 7 | * Buttons are on pins D22-D45 8 | * Axes are on pins A8-A15 9 | * Hat switches are on pins D2-D9 and D14-D21 10 | 11 | Don't forget to copy boot.py from the example folder to your CIRCUITPY drive. 12 | """ 13 | 14 | import board # type: ignore (this is a CircuitPython built-in) 15 | from joystick_xl.inputs import Axis, Button, Hat 16 | from joystick_xl.joystick import Joystick 17 | 18 | joystick = Joystick() 19 | 20 | joystick.add_input( 21 | Button(board.D22), 22 | Button(board.D23), 23 | Button(board.D24), 24 | Button(board.D25), 25 | Button(board.D26), 26 | Button(board.D27), 27 | Button(board.D28), 28 | Button(board.D29), 29 | Button(board.D30), 30 | Button(board.D31), 31 | Button(board.D32), 32 | Button(board.D33), 33 | Button(board.D34), 34 | Button(board.D35), 35 | Button(board.D36), 36 | Button(board.D37), 37 | Button(board.D38), 38 | Button(board.D39), 39 | Button(board.D40), 40 | Button(board.D41), 41 | Button(board.D42), 42 | Button(board.D43), 43 | Button(board.D44), 44 | Button(board.D45), 45 | Axis(board.A8), 46 | Axis(board.A9), 47 | Axis(board.A10), 48 | Axis(board.A11), 49 | Axis(board.A12), 50 | Axis(board.A13), 51 | Axis(board.A14), 52 | Axis(board.A15), 53 | Hat(up=board.D2, down=board.D3, left=board.D4, right=board.D5), 54 | Hat(up=board.D6, down=board.D7, left=board.D8, right=board.D9), 55 | Hat(up=board.D14, down=board.D15, left=board.D16, right=board.D17), 56 | Hat(up=board.D18, down=board.D19, left=board.D20, right=board.D21), 57 | ) 58 | 59 | while True: 60 | joystick.update() 61 | -------------------------------------------------------------------------------- /examples/3_button_operations/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore (this is a CircuitPython built-in) 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=2, buttons=2, hats=0), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/3_button_operations/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #3 - Button Operations (2 axes, 2 buttons). 3 | 4 | This example demonstrates the use of button `bypass`, `is_pressed`, `is_released`, 5 | `was_pressed` and `was_released` attributes. 6 | 7 | Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython 8 | boards with a sufficient quantity/type of pins. 9 | 10 | * Buttons are on pins D9 and D10 11 | * Axes are on pins A2 and A3 12 | * A "safety switch" is connected to pin D11 13 | * An LED and current-limiting resistor are connected to pin D12 14 | * The on-board LED connected to pin D13 is used as well 15 | 16 | Don't forget to copy boot.py from the example folder to your CIRCUITPY drive. 17 | """ 18 | 19 | import board # type: ignore (this is a CircuitPython built-in) 20 | import digitalio # type: ignore (this is a CircuitPython built-in) 21 | from joystick_xl.inputs import Axis, Button 22 | from joystick_xl.joystick import Joystick 23 | 24 | joystick = Joystick() 25 | 26 | joystick.add_input( 27 | Button(board.D9), # primary fire 28 | Button(board.D10), # secondary fire 29 | Axis(board.A2), # x-axis 30 | Axis(board.A3), # y-axis 31 | ) 32 | 33 | # The safety switch will be used to lock out the first two buttons, which - on a typical 34 | # flight stick - are the fire buttons for primary and secondary weapons systems. 35 | safety_switch = digitalio.DigitalInOut(board.D11) 36 | safety_switch.direction = digitalio.Direction.INPUT 37 | safety_switch.pull = digitalio.Pull.UP 38 | 39 | # This will be used to demonstrate the `is_pressed` attribute - it will stay lit while 40 | # button 1 is pressed. 41 | b1_led = digitalio.DigitalInOut(board.D12) 42 | b1_led.direction = digitalio.Direction.OUTPUT 43 | 44 | # This will be used to demonstrate the `is_released` attribute - it will stay lit while 45 | # button 2 is released. 46 | b2_led = digitalio.DigitalInOut(board.D13) 47 | b2_led.direction = digitalio.Direction.OUTPUT 48 | 49 | 50 | while True: 51 | 52 | # Set the bypass value of the first two buttons based on the safety switch value. 53 | for b in range(2): 54 | joystick.button[b].bypass = safety_switch.value 55 | 56 | # Axes and hat switches can also be bypassed: 57 | # joystick.axis[0].bypass = True 58 | # joystick.hat[2] = True 59 | 60 | # Hat switch buttons can also be individually bypassed like so: 61 | # joystick.hat[1].up.bypass = True 62 | # joystick.hat[1].right.bypass = True 63 | 64 | # Update the leds using `is_pressed` and `is_released`. Don't forget the list of 65 | # buttons is zero-based, so button 1 is joystick.button[0]! 66 | b1_led.value = joystick.button[0].is_pressed 67 | b2_led.value = joystick.button[1].is_released 68 | 69 | # Notice that the `*_pressed` and `*_released` events are not affected by the 70 | # `bypass` attribute - `bypass` only affects the state of the button that is sent 71 | # to the host device via USB. If you need to stop local button-related functions 72 | # (such as the LED controls above), you can wrap it in an if statement like the 73 | # following (You'll need to be connected via serial console to see the output of 74 | # the print statement): 75 | 76 | if joystick.button[1].bypass is False: 77 | if joystick.button[1].is_pressed: 78 | print("Button 2 is pressed and is not bypassed.") 79 | 80 | # If you want events to occur only on the rising/falling edge of button presses, 81 | # you can use the `was_pressed` and `was_released` attributes. (You'll need to be 82 | # connected via serial console to see the output of these print statements.) 83 | if joystick.button[0].was_pressed: 84 | print("Button 1 was just pressed.") 85 | 86 | if joystick.button[0].was_released: 87 | print("Button 1 was just released.") 88 | 89 | # Update all of the joystick inputs. 90 | joystick.update() 91 | -------------------------------------------------------------------------------- /examples/4_gpio_expander/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=0, buttons=8, hats=1), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/4_gpio_expander/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #4 - GPIO Expander (8 buttons and 1 hat switch). 3 | 4 | This example uses a Microchip MCP23017-E/SP I/O expander 5 | (https://www.adafruit.com/product/732), and requires the `adafruit_mcp230xx` and 6 | `adafruit_bus_device` libraries from the CircuitPython Library Bundle. 7 | 8 | Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython 9 | boards with a sufficient quantity/type of pins. 10 | 11 | * 3V from CircuitPython board to MCP23017 Vdd and !Reset pins 12 | * G from CircuitPython board to MCP23017 Vss and address (A0, A1, A2) pins 13 | * SCL, SDA from CircuitPython board to MCP23017 (with 10k pull-up resistors) 14 | * Buttons are on MCP23017 pins GPA0-GPA7 15 | * Hat Switch is on MCP23017 pins GPB0-GPB3 (GPB0=UP, GPB1=DOWN, GPB2=LEFT, GPB3=RIGHT) 16 | 17 | Don't forget to copy boot.py from the example folder to your CIRCUITPY drive. 18 | """ 19 | 20 | 21 | import board # type: ignore (this is a CircuitPython built-in) 22 | import busio # type: ignore (this is a CircuitPython built-in) 23 | import digitalio # type: ignore (this is a CircuitPython built-in) 24 | from adafruit_mcp230xx.mcp23017 import MCP23017 25 | from joystick_xl.inputs import Button, Hat 26 | from joystick_xl.joystick import Joystick 27 | 28 | # Set up I2C MCP23017 I/O expander 29 | i2c = busio.I2C(board.SCL, board.SDA) 30 | mcp = MCP23017(i2c) 31 | 32 | # JoystickXL doesn't know how to configure I/O pins on peripheral devices like it does 33 | # with on-board pins, so we'll have to do the set up manually here. 34 | for i in range(12): 35 | pin = mcp.get_pin(i) 36 | pin.direction = digitalio.Direction.INPUT 37 | pin.pull = digitalio.Pull.UP 38 | 39 | # Set up JoystickXL! 40 | js = Joystick() 41 | 42 | for i in range(8): 43 | js.add_input(Button(mcp.get_pin(i))) 44 | 45 | js.add_input( 46 | Hat( 47 | up=mcp.get_pin(8), 48 | down=mcp.get_pin(9), 49 | left=mcp.get_pin(10), 50 | right=mcp.get_pin(11), 51 | ) 52 | ) 53 | 54 | while True: 55 | js.update() 56 | -------------------------------------------------------------------------------- /examples/5_external_adc/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=8, buttons=0, hats=0), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/5_external_adc/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #5 - External Analog-to-Digital Converter (8 axes). 3 | 4 | This example uses a Microchip MCP3008-I/P analog-to-digital converter 5 | (https://www.adafruit.com/product/856), and requires the `adafruit_mcp3xxx` and 6 | `adafruit_bus_device` libraries from the CircuitPython Library Bundle. 7 | 8 | Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython 9 | boards with a sufficient quantity/type of pins. 10 | 11 | * 3V from CircuitPython board to MCP3008 Vdd and Vref 12 | * G from CircuitPython board to MCP3008 AGND and DGND 13 | * MOSI from CircuitPython board to MCP3008 Din 14 | * MISO from CircuitPython board to MCP3008 Dout 15 | * SCK from CircuitPython board to MCP3008 CLK 16 | * D7 from CircuitPython board to MCP3008 !CS/SHDN 17 | * Axes are on MCP3008 pins CH0-CH7 18 | 19 | Don't forget to copy boot.py from the example folder to your CIRCUITPY drive. 20 | """ 21 | 22 | import adafruit_mcp3xxx.mcp3008 as MCP 23 | import board # type: ignore (this is a CircuitPython built-in) 24 | import busio # type: ignore (this is a CircuitPython built-in) 25 | import digitalio # type: ignore (this is a CircuitPython built-in) 26 | from adafruit_mcp3xxx.analog_in import AnalogIn 27 | from joystick_xl.inputs import Axis 28 | from joystick_xl.joystick import Joystick 29 | 30 | # Set up SPI MCP3008 Analog-to-Digital converter 31 | spi = busio.SPI(clock=board.SCK, MISO=board.MISO, MOSI=board.MOSI) 32 | cs = digitalio.DigitalInOut(board.D7) 33 | mcp = MCP.MCP3008(spi, cs) 34 | 35 | # Set up JoystickXL! 36 | js = Joystick() 37 | 38 | js.add_input( 39 | Axis(AnalogIn(mcp, MCP.P0)), 40 | Axis(AnalogIn(mcp, MCP.P1)), 41 | Axis(AnalogIn(mcp, MCP.P2)), 42 | Axis(AnalogIn(mcp, MCP.P3)), 43 | Axis(AnalogIn(mcp, MCP.P4)), 44 | Axis(AnalogIn(mcp, MCP.P5)), 45 | Axis(AnalogIn(mcp, MCP.P6)), 46 | Axis(AnalogIn(mcp, MCP.P7)), 47 | ) 48 | 49 | while True: 50 | js.update() 51 | -------------------------------------------------------------------------------- /examples/6_capacitive_touch/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=0, buttons=8, hats=1), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/6_capacitive_touch/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #6 - Capacitive Touch (8 buttons and 1 hat switch). 3 | 4 | This example uses an MPR121 12-Key Capacitive Touch Sensor Breakout 5 | (https://www.adafruit.com/product/1982), and requires the `adafruit_mpr121` and 6 | `adafruit_bus_device` libraries from the CircuitPython Library Bundle. 7 | 8 | Tested on an Adafruit Metro M4 Express, but should work on other CircuitPython 9 | boards with a sufficient quantity/type of pins. 10 | 11 | * 3V, G, SCL, SDA from CircuitPython board to MPR121 board 12 | * Buttons are on MPR121 inputs 0-7 13 | * Hat Switch is on MPR121 inputs 8-11 (8=UP, 9=DOWN, 10=LEFT, 11=RIGHT) 14 | 15 | Don't forget to copy boot.py from the example folder to your CIRCUITPY drive. 16 | """ 17 | 18 | import adafruit_mpr121 19 | import board # type: ignore (this is a CircuitPython built-in) 20 | import busio # type: ignore (this is a CircuitPython built-in) 21 | from joystick_xl.inputs import Button, Hat 22 | from joystick_xl.joystick import Joystick 23 | 24 | # Set up I2C MPR121 capacitive touch sensor 25 | i2c = busio.I2C(board.SCL, board.SDA) 26 | mpr121 = adafruit_mpr121.MPR121(i2c) 27 | 28 | # Set up JoystickXL! 29 | js = Joystick() 30 | 31 | # The MPR121 library returns True when a capacitive touch channel is activated. This 32 | # makes it "active high", so we set `active_low` to False 33 | for i in range(8): 34 | js.add_input(Button(mpr121[i], active_low=False)) 35 | 36 | js.add_input( 37 | Hat( 38 | up=mpr121[8], 39 | down=mpr121[9], 40 | left=mpr121[10], 41 | right=mpr121[11], 42 | active_low=False, 43 | ) 44 | ) 45 | 46 | while True: 47 | js.update() 48 | -------------------------------------------------------------------------------- /examples/7_hotas/stick/boot.py: -------------------------------------------------------------------------------- 1 | """JoystickXL standard boot.py.""" 2 | 3 | import usb_hid # type: ignore (this is a CircuitPython built-in) 4 | from joystick_xl.hid import create_joystick 5 | 6 | # This will enable all of the standard CircuitPython USB HID devices along with a 7 | # USB HID joystick. 8 | usb_hid.enable( 9 | ( 10 | usb_hid.Device.KEYBOARD, 11 | usb_hid.Device.MOUSE, 12 | usb_hid.Device.CONSUMER_CONTROL, 13 | create_joystick(axes=8, buttons=32, hats=4), 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/7_hotas/stick/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #6 - HOTAS (Hands On Throttle And Stick) Stick Component. 3 | 4 | Tested on an Adafruit Grand Central M4 Express, but should work on other CircuitPython 5 | boards with a sufficient quantity/type of pins. 6 | 7 | * Stick buttons are on pins D22-D37 8 | * Stick axes are on pins A8-A11 9 | * Stick hat switches are on pins D14-D21 10 | 11 | The stick board needs to be connected to the throttle board as follows: 12 | 13 | * Stick TX to Throttle RX 14 | * Stick RX to Throttle TX 15 | * Stick GND to Throttle GND 16 | 17 | Don't forget to copy boot.py from the example folder to your CIRCUITPY drive. 18 | """ 19 | 20 | import struct 21 | 22 | import board # type: ignore (This is a CircuitPython built-in) 23 | import busio # type: ignore (This too!) 24 | from joystick_xl.inputs import Axis, Button, Hat 25 | from joystick_xl.joystick import Joystick 26 | 27 | # Prepare serial (UART) comms to communicate with throttle component. 28 | uart = busio.UART(board.TX, board.RX, baudrate=115200, timeout=0.1) 29 | 30 | # Serial protocol constants. 31 | STX = 0x02 # start-of-transmission 32 | ETX = 0x03 # end-of-transmission 33 | REQ = 0x05 # data request 34 | 35 | # Axis configuration constants 36 | AXIS_DB = 2500 # Deadband to apply to axis center points. 37 | AXIS_MIN = 250 # Minimum raw axis value. 38 | AXIS_MAX = 65285 # Maximum raw axis value. 39 | 40 | # Prepare USB HID HOTAS device. 41 | hotas = Joystick() 42 | 43 | hotas.add_input( 44 | # The first 16 buttons are local I/O associated with the stick component, which 45 | # connects to the host via USB. 46 | Button(board.D22), 47 | Button(board.D23), 48 | Button(board.D24), 49 | Button(board.D25), 50 | Button(board.D26), 51 | Button(board.D27), 52 | Button(board.D28), 53 | Button(board.D29), 54 | Button(board.D30), 55 | Button(board.D31), 56 | Button(board.D32), 57 | Button(board.D33), 58 | Button(board.D34), 59 | Button(board.D35), 60 | Button(board.D36), 61 | Button(board.D37), 62 | # The last set of 16 buttons are virtual I/O associated with the throttle 63 | # component, which connects to the stick component via serial. 64 | Button(), 65 | Button(), 66 | Button(), 67 | Button(), 68 | Button(), 69 | Button(), 70 | Button(), 71 | Button(), 72 | Button(), 73 | Button(), 74 | Button(), 75 | Button(), 76 | Button(), 77 | Button(), 78 | Button(), 79 | Button(), 80 | # The first 4 axes are local I/O associated with the stick component. 81 | Axis(board.A8, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 82 | Axis(board.A9, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 83 | Axis(board.A10, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 84 | Axis(board.A11, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 85 | # The last 4 axes are virtual I/O associated with the throttle component. 86 | Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 87 | Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 88 | Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 89 | Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX), 90 | # The first 2 hat switches are local I/O associated with the stick component. 91 | Hat(up=board.D14, down=board.D15, left=board.D16, right=board.D17), 92 | Hat(up=board.D18, down=board.D19, left=board.D20, right=board.D21), 93 | # The last 2 hat switches are virtual I/O associated with the throttle component. 94 | Hat(), 95 | Hat(), 96 | ) 97 | 98 | # Stick and Throttle input processing loop. 99 | while True: 100 | 101 | # Request raw state data from the throttle component. 102 | uart.write(bytearray((STX, REQ, ETX))) 103 | 104 | # we're looking for exactly 15 bytes of data from the throttle: 105 | # Byte 0 = STX 106 | # Byte 1 = REQ 107 | # Bytes 2-3 = 16 bits of button data 108 | # Bytes 4-11 = 4 x 16 bits of axis data 109 | # Byte 12 = 2 x 4 bits of hat switch data 110 | # Byte 13 = Checkbyte (for rudimentary error checking) 111 | # Byte 14 = ETX 112 | rx = uart.read(15) 113 | 114 | if rx is not None and len(rx) == 15: 115 | 116 | # Calculate correct checkbyte value by XORing all command and data bytes. 117 | # Framing bytes (STX/ETX) and the incoming checkbyte are excluded. 118 | checkbyte = 0x00 119 | for b in rx[1:13]: 120 | checkbyte ^= b 121 | 122 | # Continue processing if framing and checkbyte are correct. 123 | if rx[0] == STX and rx[14] == ETX and rx[13] == checkbyte: 124 | 125 | # At this point there is only one 'command' to process - a data request. 126 | if rx[1] == REQ: 127 | 128 | # Unpack raw data. 129 | data = struct.unpack_from("> i) & 0x01 134 | 135 | # Update axis virtual inputs with raw states from throttle. 136 | for i in range(4): 137 | hotas.axis[i + 4].source_value = data[1 + i] 138 | 139 | # update hat switch virtual inputs with raw states from throttle. 140 | for i in range(2): 141 | hotas.hat[i + 2].unpack_source_values(data[5] >> (4 * i)) 142 | 143 | # At this point we have collected all remote data and can update everything. 144 | hotas.update() 145 | -------------------------------------------------------------------------------- /examples/7_hotas/throttle/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL Example #6 - HOTAS (Hands On Throttle And Stick) Throttle Component. 3 | 4 | Tested on an Adafruit Grand Central M4 Express, but should work on other CircuitPython 5 | boards with a sufficient quantity/type of pins. 6 | 7 | * Throttle buttons are on pins D22-D37 8 | * Throttle axes are on pins A8-A11 9 | * Throttle hat switches are on pins D14-D21 10 | 11 | The throttle board needs to be connected to the stick board as follows: 12 | 13 | * Throttle TX to Stick RX 14 | * Throttle RX to Stick TX 15 | * Throttle GND to Stick GND 16 | 17 | You don't need to copy boot.py to the throttle component - all USB HID communication 18 | is handled by the stick component. 19 | """ 20 | 21 | import struct 22 | 23 | import board # type: ignore (This is a CircuitPython built-in) 24 | import busio # type: ignore (This too!) 25 | from joystick_xl.inputs import Axis, Button, Hat 26 | 27 | # Prepare serial (UART) comms to communicate with stick component. 28 | uart = busio.UART(board.TX, board.RX, baudrate=115200) 29 | 30 | # Serial protocol constants. 31 | STX = 0x02 # start-of-transmission 32 | ETX = 0x03 # end-of-transmission 33 | REQ = 0x05 # data request 34 | 35 | # Set up local I/O. 36 | buttons = [ 37 | Button(board.D22), 38 | Button(board.D23), 39 | Button(board.D24), 40 | Button(board.D25), 41 | Button(board.D26), 42 | Button(board.D27), 43 | Button(board.D28), 44 | Button(board.D29), 45 | Button(board.D30), 46 | Button(board.D31), 47 | Button(board.D32), 48 | Button(board.D33), 49 | Button(board.D34), 50 | Button(board.D35), 51 | Button(board.D36), 52 | Button(board.D37), 53 | ] 54 | 55 | axes = [ 56 | # We are only interested in raw values here - deadband/min/max processing will 57 | # be handled in the code running on the stick component. 58 | Axis(board.A8), 59 | Axis(board.A9), 60 | Axis(board.A10), 61 | Axis(board.A11), 62 | ] 63 | 64 | hats = [ 65 | Hat(up=board.D14, down=board.D15, left=board.D16, right=board.D17), 66 | Hat(up=board.D18, down=board.D19, left=board.D20, right=board.D21), 67 | ] 68 | 69 | # Throttle input processing loop. 70 | while True: 71 | 72 | # There's nothing to do unless we've received at least 3 bytes. 73 | # Byte 0 = STX 74 | # Byte 1 = Command Byte (Current only REQ is implemented) 75 | # Byte 2 = ETX 76 | if uart.in_waiting >= 3: 77 | rx = uart.read(3) # Read 3 bytes (stx, command, etx). 78 | 79 | # Make sure no framing error has occurred (missing STX or ETX). 80 | if rx[0] != STX or rx[2] != ETX: 81 | uart.reset_input_buffer() 82 | continue 83 | 84 | # Process state request command. 85 | if rx[1] == REQ: 86 | 87 | # Collect raw button states. 88 | button_states = 0 89 | for i, b in enumerate(buttons): 90 | if b.source_value: 91 | button_states |= 0x01 << i 92 | 93 | # Collect raw hat switch states. 94 | hat_states = 0 95 | for i, h in enumerate(hats): 96 | hat_states |= h.packed_source_values << (4 * i) 97 | 98 | # Pack up all the data in a byte array. 99 | tx = bytearray(15) 100 | struct.pack_into( 101 | " 2 | USBlyzer Report 3 | 42 | 43 | 44 | 45 |

USB Input Device

46 | 47 | 48 | 49 | 50 | 51 | 52 |
Connection StatusDevice connected
Current Configuration1
SpeedLow (1.5 Mbit/s)
Device Address4
Number Of Open Pipes1
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
OffsetFieldSizeValueDescription
0bLength112h
1bDescriptorType101hDevice
2bcdUSB20110hUSB Spec 1.1
4bDeviceClass100hClass info in Ifc Descriptors
5bDeviceSubClass100h
6bDeviceProtocol100h
7bMaxPacketSize0108h8 bytes
8idVendor20583hPadix Co., Ltd (Rockfire)
10idProduct22060h
12bcdDevice20102h1.02
14iManufacturer101h"Padix Co. Ltd."
15iProduct102h"USB,2-axis 8-button gamepad"
16iSerialNumber100h
17bNumConfigurations101h
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType102hConfiguration
2wTotalLength20022h
4bNumInterfaces101h
5bConfigurationValue101h
6iConfiguration100h
7bmAttributes180hBus Powered
4..0: Reserved...00000 
5: Remote Wakeup..0..... No
6: Self Powered.0...... No, Bus Powered
7: Reserved (set to one)
(bus-powered for 1.0)
1....... 
8bMaxPower132h100 mA
243 |
244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber100h
3bAlternateSetting100h
4bNumEndpoints101h
5bInterfaceClass103hHID
6bInterfaceSubClass100h
7bInterfaceProtocol100h
8iInterface100h
311 |
312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType121hHID
2bcdHID20110h1.10
4bCountryCode121hUS
5bNumDescriptors101h
6bDescriptorType122hReport
7wDescriptorLength2004Dh77 bytes
365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 |
OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress181h1 In
3bmAttributes103hInterrupt
1..0: Transfer Type......11 Interrupt
7..2: Reserved000000.. 
4wMaxPacketSize20008h8 bytes
6bInterval10Ah10 ms
425 |
426 |
427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 |
Item Tag (Value)Raw Data
Usage Page (Generic Desktop)05 01 
Usage (Joystick)09 04 
Collection (Application)A1 01 
    Usage (Pointer)09 01 
    Collection (Physical)A1 00 
        Usage (X)09 30 
        Usage (Y)09 31 
        Logical Minimum (0)15 00 
        Logical Maximum (255)26 FF 00 
        Report Size (8)75 08 
        Report Count (2)95 02 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    End CollectionC0 
    Usage Page (Button)05 09 
    Usage Minimum (Button 1)19 01 
    Usage Maximum (Button 8)29 08 
    Logical Minimum (0)15 00 
    Logical Maximum (1)25 01 
    Report Size (1)75 01 
    Report Count (8)95 08 
    Unit Exponent (0)55 00 
    Unit (None)65 00 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    Report Size (1)75 01 
    Report Count (40)95 28 
    Input (Cnst,Ary,Abs)81 01 
    Usage Page (Bar Code Scanner)05 8C 
    Usage09 01 
    Collection (Physical)A1 00 
        Usage09 02 
        Logical Minimum (0)15 00 
        Logical Maximum (255)26 FF 00 
        Report Size (8)75 08 
        Report Count (4)95 04 
        Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)91 02 
        Usage09 02 
        Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)B1 02 
    End CollectionC0 
End CollectionC0 
590 |

This report was generated by USBlyzer

591 | 592 | 593 | -------------------------------------------------------------------------------- /extras/hid_descriptors/images/buffalo_gamepad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasteddy516/CircuitPython_JoystickXL/9be137f80a1a268e47e4620e1310cf1bdbe5c45a/extras/hid_descriptors/images/buffalo_gamepad.jpg -------------------------------------------------------------------------------- /extras/hid_descriptors/images/logitech_x52_pro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasteddy516/CircuitPython_JoystickXL/9be137f80a1a268e47e4620e1310cf1bdbe5c45a/extras/hid_descriptors/images/logitech_x52_pro.jpg -------------------------------------------------------------------------------- /extras/hid_descriptors/images/nvidia_shield.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasteddy516/CircuitPython_JoystickXL/9be137f80a1a268e47e4620e1310cf1bdbe5c45a/extras/hid_descriptors/images/nvidia_shield.jpg -------------------------------------------------------------------------------- /extras/hid_descriptors/images/sony_ds4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fasteddy516/CircuitPython_JoystickXL/9be137f80a1a268e47e4620e1310cf1bdbe5c45a/extras/hid_descriptors/images/sony_ds4.jpg -------------------------------------------------------------------------------- /extras/hid_descriptors/logitech_x52_pro.html: -------------------------------------------------------------------------------- 1 | 2 | USBlyzer Report 3 | 42 | 43 | 44 | 45 |

USB Input Device

46 | 47 | 48 | 49 | 50 | 51 | 52 |
Connection StatusDevice connected
Current Configuration1
SpeedFull (12 Mbit/s)
Device Address8
Number Of Open Pipes1
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
OffsetFieldSizeValueDescription
0bLength112h
1bDescriptorType101hDevice
2bcdUSB20200hUSB Spec 2.0
4bDeviceClass100hClass info in Ifc Descriptors
5bDeviceSubClass100h
6bDeviceProtocol100h
7bMaxPacketSize0108h8 bytes
8idVendor206A3hSaitek PLC
10idProduct20762h
12bcdDevice20123h1.23
14iManufacturer101h"Saitek"
15iProduct102h"Saitek X52 Pro Flight Control System"
16iSerialNumber100h
17bNumConfigurations101h
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType102hConfiguration
2wTotalLength20022h
4bNumInterfaces101h
5bConfigurationValue101h
6iConfiguration100h
7bmAttributes180hBus Powered
4..0: Reserved...00000 
5: Remote Wakeup..0..... No
6: Self Powered.0...... No, Bus Powered
7: Reserved (set to one)
(bus-powered for 1.0)
1....... 
8bMaxPower173h230 mA
243 |
244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber100h
3bAlternateSetting100h
4bNumEndpoints101h
5bInterfaceClass103hHID
6bInterfaceSubClass100h
7bInterfaceProtocol100h
8iInterface100h
311 |
312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType121hHID
2bcdHID20111h1.11
4bCountryCode100h
5bNumDescriptors101h
6bDescriptorType122hReport
7wDescriptorLength2007Dh125 bytes
365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 |
OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress181h1 In
3bmAttributes103hInterrupt
1..0: Transfer Type......11 Interrupt
7..2: Reserved000000.. 
4wMaxPacketSize20010h16 bytes
6bInterval10Ah10 ms
425 |
426 |
427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 |
Item Tag (Value)Raw Data
Usage Page (Generic Desktop)05 01 
Usage (Joystick)09 04 
Collection (Application)A1 01 
    Usage (Pointer)09 01 
    Collection (Physical)A1 00 
        Usage (X)09 30 
        Usage (Y)09 31 
        Logical Minimum (0)15 00 
        Logical Maximum (1023)26 FF 03 
        Report Size (10)75 0A 
        Report Count (2)95 02 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
        Report Size (2)75 02 
        Report Count (1)95 01 
        Input (Cnst,Ary,Abs)81 01 
        Usage (Rz)09 35 
        Logical Minimum (0)15 00 
        Logical Maximum (1023)26 FF 03 
        Report Size (10)75 0A 
        Report Count (1)95 01 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
        Usage (Z)09 32 
        Usage (Rx)09 33 
        Usage (Ry)09 34 
        Usage (Slider)09 36 
        Logical Minimum (0)15 00 
        Logical Maximum (255)26 FF 00 
        Report Size (8)75 08 
        Report Count (4)95 04 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
        Usage Page (Button)05 09 
        Usage Minimum (Button 1)19 01 
        Usage Maximum (Button 39)29 27 
        Logical Minimum (0)15 00 
        Logical Maximum (1)25 01 
        Report Count (39)95 27 
        Report Size (1)75 01 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
        Report Size (5)75 05 
        Report Count (1)95 01 
        Input (Cnst,Ary,Abs)81 01 
        Usage Page (Generic Desktop)05 01 
        Usage (Hat Switch)09 39 
        Logical Minimum (1)15 01 
        Logical Maximum (8)25 08 
        Physical Minimum (0)35 00 
        Physical Maximum (315)46 3B 01 
        Unit (Eng Rot: Degree)66 14 00 
        Report Size (4)75 04 
        Report Count (1)95 01 
        Input (Data,Var,Abs,NWrp,Lin,Pref,Null,Bit)81 42 
        Usage Page (Game Controls)05 05 
        Usage (Move Right/Left)09 24 
        Usage (Move Up/Down)09 26 
        Logical Minimum (0)15 00 
        Logical Maximum (15)25 0F 
        Report Size (4)75 04 
        Report Count (2)95 02 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    End CollectionC0 
End CollectionC0 
678 |

This report was generated by USBlyzer

679 | 680 | 681 | -------------------------------------------------------------------------------- /extras/hid_descriptors/nvidia_shield_controller.html: -------------------------------------------------------------------------------- 1 | 2 | USBlyzer Report 3 | 42 | 43 | 44 | 45 |

USB Input Device

46 | 47 | 48 | 49 | 50 | 51 | 52 |
Connection StatusDevice connected
Current Configuration1
SpeedFull (12 Mbit/s)
Device Address5
Number Of Open Pipes1
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
OffsetFieldSizeValueDescription
0bLength112h
1bDescriptorType101hDevice
2bcdUSB20200hUSB Spec 2.0
4bDeviceClass100hClass info in Ifc Descriptors
5bDeviceSubClass100h
6bDeviceProtocol100h
7bMaxPacketSize0108h8 bytes
8idVendor20955hNVidia Corp.
10idProduct27210h
12bcdDevice20100h1.00
14iManufacturer10Bh"NVIDIA Corporation"
15iProduct10Ch"NVIDIA Controller v01.03"
16iSerialNumber100h
17bNumConfigurations101h
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType102hConfiguration
2wTotalLength20022h
4bNumInterfaces101h
5bConfigurationValue101h
6iConfiguration100h
7bmAttributes180hBus Powered
4..0: Reserved...00000 
5: Remote Wakeup..0..... No
6: Self Powered.0...... No, Bus Powered
7: Reserved (set to one)
(bus-powered for 1.0)
1....... 
8bMaxPower1FAh500 mA
243 |
244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber100h
3bAlternateSetting100h
4bNumEndpoints101h
5bInterfaceClass103hHID
6bInterfaceSubClass100h
7bInterfaceProtocol100h
8iInterface10Eh"Gamepad1 Device."
311 |
312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 |
OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType121hHID
2bcdHID20111h1.11
4bCountryCode100h
5bNumDescriptors101h
6bDescriptorType122hReport
7wDescriptorLength200F1h241 bytes
365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 |
OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress181h1 In
3bmAttributes103hInterrupt
1..0: Transfer Type......11 Interrupt
7..2: Reserved000000.. 
4wMaxPacketSize20040h64 bytes
6bInterval102h2 ms
425 |
426 |
427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 |
Item Tag (Value)Raw Data
Usage Page (Generic Desktop)05 01 
Logical Minimum (0)15 00 
Usage (Game Pad)09 05 
Collection (Application)A1 01 
    Report ID (1)85 01 
    Usage Page (Button)05 09 
    Logical Minimum (0)15 00 
    Logical Maximum (1)25 01 
    Report Size (1)75 01 
    Report Count (10)95 0A 
    Usage (Button 1)09 01 
    Usage (Button 2)09 02 
    Usage (Button 4)09 04 
    Usage (Button 5)09 05 
    Usage (Button 7)09 07 
    Usage (Button 8)09 08 
    Usage (Button 14)09 0E 
    Usage (Button 15)09 0F 
    Usage (Button 9)09 09 
    Usage (Button 12)09 0C 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    Usage Page (Consumer Devices)05 0C 
    Report Count (6)95 06 
    Usage (Mute)09 E2 
    Usage (Volume Increment)09 E9 
    Usage (Volume Decrement)09 EA 
    Usage (Power)09 30 
    Usage (AC Back)0A 24 02 
    Usage (AC Home)0A 23 02 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    Usage Page (Generic Desktop)05 01 
    Usage (Hat Switch)09 39 
    Logical Maximum (7)25 07 
    Physical Minimum (0)35 00 
    Physical Maximum (270)46 0E 01 
    Unit (Eng Rot: Degree)65 14 
    Report Size (4)75 04 
    Report Count (1)95 01 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    Input (Cnst,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 03 
    Usage (Pointer)09 01 
    Collection (Physical)A1 00 
        Report Size (16)75 10 
        Report Count (4)95 04 
        Logical Minimum (0)15 00 
        Logical Maximum (-1)26 FF FF 
        Physical Minimum (0)35 00 
        Physical Maximum (-1)46 FF FF 
        Usage (X)09 30 
        Usage (Y)09 31 
        Usage (Z)09 32 
        Usage (Rz)09 35 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
        Usage Page (Simulation Controls)05 02 
        Report Count (2)95 02 
        Usage (Brake)09 C5 
        Usage (Accelerator)09 C4 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    End CollectionC0 
    Collection (Application)A1 01 
        Usage Minimum (Flight Simulation Device)19 01 
        Usage Maximum (Tank Simulation Device)29 03 
        Logical Maximum (-1)26 FF FF 
        Report Count (3)95 03 
        Report Size (16)75 10 
        Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)91 02 
    End CollectionC0 
End CollectionC0 
Usage Page (Generic Desktop)05 01 
Usage (Mouse)09 02 
Collection (Application)A1 01 
    Report ID (2)85 02 
    Usage (Pointer)09 01 
    Collection (Physical)A1 00 
        Usage Page (Button)05 09 
        Usage Minimum (Button 1)19 01 
        Usage Maximum (Button 3)29 03 
        Logical Maximum (1)25 01 
        Report Size (1)75 01 
        Report Count (3)95 03 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
        Usage Page (Button)05 09 
        Usage (Button 5)09 05 
        Report Count (1)95 01 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
        Report Size (4)75 04 
        Input (Cnst,Ary,Abs)81 01 
        Usage Page (Generic Desktop)05 01 
        Usage (X)09 30 
        Usage (Y)09 31 
        Logical Minimum (-127)15 81 
        Logical Maximum (127)25 7F 
        Report Size (16)75 10 
        Report Count (2)95 02 
        Input (Data,Var,Rel,NWrp,Lin,Pref,NNul,Bit)81 06 
    End CollectionC0 
End CollectionC0 
Usage Page (Vendor-Defined 223)06 DE FF 
Usage (Vendor-Defined 1)09 01 
Collection (Application)A1 01 
    Usage Page05 FF 
    Usage Minimum19 01 
    Usage Maximum29 40 
    Report ID (253)85 FD 
    Logical Minimum (0)15 00 
    Logical Maximum (-1)25 FF 
    Report Count (64)95 40 
    Report Size (8)75 08 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
End CollectionC0 
Usage Page (Vendor-Defined 223)06 DE FF 
Usage (Vendor-Defined 3)09 03 
Collection (Application)A1 01 
    Usage Minimum (Vendor-Defined 1)19 01 
    Usage Maximum (Vendor-Defined 64)29 40 
    Report ID (252)85 FC 
    Report Count (64)95 40 
    Report Size (8)75 08 
    Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)B1 02 
End CollectionC0 
914 |

This report was generated by USBlyzer

915 | 916 | 917 | -------------------------------------------------------------------------------- /extras/scripts/axis_visualizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Display a graph that shows axis output values across the range of input values. 3 | 4 | Yes, there is a nasty bit of path manipulation to allow importing of joystick_xl 5 | modules from this folder. Sorry. :P 6 | """ 7 | 8 | # Adjust the min/max/deadband values to suit your test case. 9 | min = 5000 10 | max = 64535 11 | deadband = 2500 12 | 13 | # nasty path manipulation 14 | import os # noqa: E402 15 | import sys # noqa: E402 16 | 17 | sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) 18 | 19 | try: 20 | from joystick_xl.inputs import Axis 21 | except ImportError: 22 | print("*** ERROR: Can't import joystick_xl modules. ***") 23 | sys.exit() 24 | 25 | try: 26 | from matplotlib import pyplot as plt 27 | except ImportError: 28 | print("*** ERROR: matplotlib is required to display graphs. ***") 29 | 30 | # readability constants 31 | AXIS = 0 32 | OUTPUT = 1 33 | NODB = 0 34 | NORMAL = 1 35 | INVERTED = 2 36 | 37 | # generate axis data 38 | input = [i for i in range(65536)] 39 | axes = [ 40 | [Axis(min=min, max=max), []], 41 | [Axis(deadband=deadband, min=min, max=max, invert=False), []], 42 | [Axis(deadband=deadband, min=min, max=max, invert=True), []], 43 | ] 44 | for i in input: 45 | for a in axes: 46 | a[AXIS].source_value = i 47 | a[OUTPUT].append(a[AXIS]._update()) 48 | 49 | # normal axis 50 | plt.plot(input, axes[NORMAL][OUTPUT], color="blue", zorder=1.0) 51 | 52 | # inverted axis 53 | plt.plot(input, axes[INVERTED][OUTPUT], color="orange", zorder=0.9) 54 | 55 | # reference line accounting for min/max but ignoring deadband 56 | plt.plot(input, axes[NODB][OUTPUT], ":", color="lightgrey", zorder=0.1) 57 | 58 | # minimum line 59 | plt.axvline(x=min, color="lightgrey", linestyle=":", zorder=0.1) 60 | 61 | # maximum line 62 | plt.axvline(x=max, color="lightgrey", linestyle=":", zorder=0.1) 63 | 64 | # center point accounting for min/max 65 | plt.axvline( 66 | x=axes[NODB][AXIS]._raw_midpoint, color="lightgrey", linestyle=":", zorder=0.1 67 | ) 68 | 69 | # labels and titles 70 | plt.title("Processed Axis Output over Entire Input Range") 71 | plt.xlabel("Raw Input Value (16-bit)") 72 | plt.xticks([0, 8192, 16384, 24576, 32768, 40960, 49152, 57344, 65535]) 73 | plt.ylabel("Processed Axis Value (8-bit)") 74 | plt.legend(["Normal", "Inverted"]) 75 | 76 | # draw it! 77 | plt.show() 78 | -------------------------------------------------------------------------------- /extras/scripts/bundle_release.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compile and bundle JoystickXL for release on Github. 3 | 4 | Yes, there is a nasty bit of path manipulation to allow importing of joystick_xl 5 | modules from this folder. Sorry. :P 6 | """ 7 | 8 | import subprocess 9 | import sys 10 | import zipfile 11 | from pathlib import Path 12 | from typing import List 13 | 14 | CP_VERSIONS = [8, 9] 15 | JXL_PATH = Path(__file__).parent.parent.parent 16 | 17 | try: 18 | sys.path.append(str(JXL_PATH)) 19 | from joystick_xl import __version__ as version 20 | except ImportError: 21 | print("*** ERROR: Can't import joystick_xl modules. ***") 22 | sys.exit() 23 | 24 | 25 | def cleanup(file_patterns: List[str], folder: Path = JXL_PATH / "joystick_xl") -> None: 26 | """Remove files matching the specified patterns from the module folder.""" 27 | for pattern in file_patterns: 28 | for file in list((JXL_PATH / "joystick_xl").glob(pattern)): 29 | file.unlink() 30 | 31 | 32 | files = list((JXL_PATH / "joystick_xl").glob("*.py")) 33 | 34 | if not files: 35 | print("*** ERROR: Could not locate joystick_xl .py files") 36 | sys.exit() 37 | 38 | print(f"Creating JoystickXL {version} bundles for CircuitPython versions {CP_VERSIONS}") 39 | 40 | for v in CP_VERSIONS: 41 | try: 42 | cleanup(["*.mpy"]) 43 | for f in files: 44 | command = [f"mpy-cross{v}", f] 45 | process = subprocess.Popen(command) 46 | process.wait() 47 | bundle_file = Path(__file__).parent / f"joystick_xl_{version}_cp{v}.zip" 48 | bundle_files = list((JXL_PATH / "joystick_xl").glob("*.mpy")) 49 | bundle_files.append(JXL_PATH / "README.rst") 50 | bundle_files.append(JXL_PATH / "LICENSE") 51 | with zipfile.ZipFile(bundle_file, "w") as zip_file: 52 | for f in bundle_files: 53 | zip_file.write(f, f"joystick_xl/{f.name}") 54 | cleanup(["*.mpy"]) 55 | print(f"+ Created {bundle_file.name} for CircuitPython {v}") 56 | except FileNotFoundError: 57 | print(f"*** ERROR: mpy-cross{v}.exe not found, skipping CircuitPython {v}") 58 | -------------------------------------------------------------------------------- /joystick_xl/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | JoystickXL USB HID library for CircuitPython. 3 | 4 | Create extra large (high input count) USB HID control devices with CircuitPython. 5 | 6 | * Author(s): Edward Wright 7 | 8 | 9 | MIT License 10 | ============================= 11 | 12 | Copyright (c) 2024 Edward Wright 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | 32 | """ 33 | 34 | __version__ = "0.4.3" 35 | -------------------------------------------------------------------------------- /joystick_xl/hid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initial USB configuration tools for use in ``boot.py`` setup. 3 | 4 | This module provides the necessary functions to create a CircuitPython USB HID device 5 | with a descriptor that includes the configured type and quantity of inputs. 6 | """ 7 | 8 | import usb_hid # type: ignore (this is a CircuitPython built-in) 9 | 10 | from joystick_xl import __version__ 11 | 12 | 13 | def create_joystick( 14 | axes: int = 4, 15 | buttons: int = 16, 16 | hats: int = 1, 17 | report_id: int = 0x04, 18 | ) -> usb_hid.Device: 19 | """ 20 | Create the ``usb_hid.Device`` required by ``usb_hid.enable()`` in ``boot.py``. 21 | 22 | .. note:: 23 | 24 | JoystickXL will add an entry to the ``boot_out.txt`` file on your ``CIRCUITPY`` 25 | drive. It is used by the ``Joystick`` module to retrieve configuration 26 | settings. 27 | 28 | :param axes: The number of axes to support, from 0 to 8. (Default is 4) 29 | :type axes: int, optional 30 | :param buttons: The number of buttons to support, from 0 to 128. (Default is 16) 31 | :type buttons: int, optional 32 | :param hats: The number of hat switches to support, from 0 to 4. (Default is 1) 33 | :type hats: int, optional 34 | :param report_id: The USB HID report ID number to use. (Default is 4) 35 | :type report_d: int, optional 36 | :return: A ``usb_hid.Device`` object with a descriptor identifying it as a joystick 37 | with the specified number of buttons, axes and hat switches. 38 | :rtype: ``usb_hid.Device`` 39 | 40 | """ 41 | _num_axes = axes 42 | _num_buttons = buttons 43 | _num_hats = hats 44 | 45 | # Validate the number of configured axes, buttons and hats. 46 | if _num_axes < 0 or _num_axes > 8: 47 | raise ValueError("Axis count must be from 0-8.") 48 | 49 | if _num_buttons < 0 or _num_buttons > 128: 50 | raise ValueError("Button count must be from 0-128.") 51 | 52 | if _num_hats < 0 or _num_hats > 4: 53 | raise ValueError("Hat count must be from 0-4.") 54 | 55 | _report_length = 0 56 | 57 | # Formatting is disabled below to allow the USB descriptor elements to be 58 | # grouped and annotated such that the descriptor is readable and maintainable. 59 | 60 | # fmt: off 61 | _descriptor = bytearray(( 62 | 0x05, 0x01, # : USAGE_PAGE (Generic Desktop) 63 | 0x09, 0x04, # : USAGE (Joystick) 64 | 0xA1, 0x01, # : COLLECTION (Application) 65 | 0x85, report_id, # : REPORT_ID (Default is 4) 66 | )) 67 | 68 | if _num_axes: 69 | for i in range(_num_axes): 70 | _descriptor.extend(bytes(( 71 | 0x09, min(0x30 + i, 0x36) # : USAGE (X,Y,Z,Rx,Ry,Rz,S0,S1) 72 | ))) 73 | 74 | _descriptor.extend(bytes(( 75 | 0x15, 0x00, # : LOGICAL_MINIMUM (0) 76 | 0x26, 0xFF, 0x00, # : LOGICAL_MAXIMUM (255) 77 | 0x75, 0x08, # : REPORT_SIZE (8) 78 | 0x95, _num_axes, # : REPORT_COUNT (num_axes) 79 | 0x81, 0x02, # : INPUT (Data,Var,Abs) 80 | ))) 81 | 82 | _report_length = _num_axes 83 | 84 | if _num_hats: 85 | for i in range(_num_hats): 86 | _descriptor.extend(bytes(( 87 | 0x09, 0x39, # : USAGE (Hat switch) 88 | ))) 89 | 90 | _descriptor.extend(bytes(( 91 | 0x15, 0x00, # : LOGICAL_MINIMUM (0) 92 | 0x25, 0x07, # : LOGICAL_MAXIMUM (7) 93 | 0x35, 0x00, # : PHYSICAL_MINIMUM (0) 94 | 0x46, 0x3B, 0x01, # : PHYSICAL_MAXIMUM (315) 95 | 0x65, 0x14, # : UNIT (Eng Rot:Angular Pos) 96 | 0x75, 0x04, # : REPORT_SIZE (4) 97 | 0x95, _num_hats, # : REPORT_COUNT (num_hats) 98 | 0x81, 0x42, # : INPUT (Data,Var,Abs,Null) 99 | ))) 100 | 101 | _hat_pad = _num_hats % 2 102 | if _hat_pad: 103 | _descriptor.extend(bytes(( 104 | 0x75, 0x04, # : REPORT_SIZE (4) 105 | 0x95, _hat_pad, # : REPORT_COUNT (_hat_pad) 106 | 0x81, 0x03, # : INPUT (Cnst,Var,Abs) 107 | ))) 108 | 109 | _report_length += ((_num_hats // 2) + bool(_hat_pad)) 110 | 111 | if _num_buttons: 112 | _descriptor.extend(bytes(( 113 | 0x05, 0x09, # : USAGE_PAGE (Button) 114 | 0x19, 0x01, # : USAGE_MINIMUM (Button 1) 115 | 0x29, _num_buttons, # : USAGE_MAXIMUM (num_buttons) 116 | 0x15, 0x00, # : LOGICAL_MINIMUM (0) 117 | 0x25, 0x01, # : LOGICAL_MAXIMUM (1) 118 | 0x95, _num_buttons, # : REPORT_COUNT (num_buttons) 119 | 0x75, 0x01, # : REPORT_SIZE (1) 120 | 0x81, 0x02, # : INPUT (Data,Var,Abs) 121 | ))) 122 | 123 | _button_pad = _num_buttons % 8 124 | if _button_pad: 125 | _descriptor.extend(bytes(( 126 | 0x75, 0x01, # : REPORT_SIZE (1) 127 | 0x95, 8 - _button_pad, # : REPORT_COUNT (_button_pad) 128 | 0x81, 0x03, # : INPUT (Cnst,Var,Abs) 129 | ))) 130 | 131 | _report_length += ((_num_buttons // 8) + bool(_button_pad)) 132 | 133 | _descriptor.extend(bytes(( 134 | 0xC0, # : END_COLLECTION 135 | ))) 136 | # fmt: on 137 | 138 | # write configuration data to boot.out using 'print' 139 | print( 140 | "+ Enabled JoystickXL", 141 | __version__, 142 | "with", 143 | _num_axes, 144 | "axes,", 145 | _num_buttons, 146 | "buttons and", 147 | _num_hats, 148 | "hats for a total of", 149 | _report_length, 150 | "report bytes.", 151 | ) 152 | 153 | return usb_hid.Device( 154 | report_descriptor=bytes(_descriptor), 155 | usage_page=0x01, # same as USAGE_PAGE from descriptor above 156 | usage=0x04, # same as USAGE from descriptor above 157 | report_ids=(report_id,), # report ID defined in descriptor 158 | in_report_lengths=(_report_length,), # length of reports to host 159 | out_report_lengths=(0,), # length of reports from host 160 | ) 161 | 162 | 163 | def _get_device() -> usb_hid.Device: 164 | """Find a JoystickXL device in the list of active USB HID devices.""" 165 | for device in usb_hid.devices: 166 | if ( 167 | device.usage_page == 0x01 168 | and device.usage == 0x04 169 | and hasattr(device, "send_report") 170 | ): 171 | return device 172 | raise ValueError("Could not find JoystickXL HID device - check boot.py.)") 173 | -------------------------------------------------------------------------------- /joystick_xl/inputs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes to simplify mapping GPIO pins and values to JoystickXL inputs and states. 3 | 4 | This module provides a set of classes to aid in configuring GPIO pins and convert 5 | their raw states to values that are usable by JoystickXL. 6 | """ 7 | 8 | # These typing imports help during development in vscode but fail in CircuitPython 9 | try: 10 | from typing import Union 11 | except ImportError: 12 | pass 13 | 14 | # These are all CircuitPython built-ins 15 | try: 16 | from analogio import AnalogIn # type: ignore 17 | from digitalio import DigitalInOut, Direction, Pull # type: ignore 18 | from microcontroller import Pin # type: ignore 19 | except ImportError: 20 | print("*** WARNING: CircuitPython built-in modules could not be imported. ***") 21 | 22 | 23 | class VirtualInput: 24 | """Provide an object with a .value property to represent a remote input.""" 25 | 26 | def __init__(self, value: Union[bool, int]) -> None: 27 | """ 28 | Provide an object with a ``.value`` property to represent a remote input. 29 | 30 | :param value: Sets the initial ``.value`` property (Should be ``True`` for 31 | active-low buttons, ``32768`` for idle/centered axes). 32 | :type value: bool or int 33 | """ 34 | self.value = value 35 | 36 | 37 | class Axis: 38 | """Data source storage and scaling/deadband processing for an axis input.""" 39 | 40 | MIN = 0 41 | """Lowest possible axis value for USB HID reports.""" 42 | 43 | MAX = 255 44 | """Highest possible axis value for USB HID reports.""" 45 | 46 | IDLE = 128 47 | """Idle/Center axis value for USB HID reports.""" 48 | 49 | X = 0 50 | """Alias for the X-axis index.""" 51 | 52 | Y = 1 53 | """Alias for the Y-axis index.""" 54 | 55 | Z = 2 56 | """Alias for the Z-axis index.""" 57 | 58 | RX = 3 59 | """Alias for the RX-axis index.""" 60 | 61 | RY = 4 62 | """Alias for the RY-axis index.""" 63 | 64 | RZ = 5 65 | """Alias for the RZ-axis index.""" 66 | 67 | S0 = 6 68 | """Alias for the S0-axis index.""" 69 | 70 | S1 = 7 71 | """Alias for the S1-axis index.""" 72 | 73 | @property 74 | def value(self) -> int: 75 | """ 76 | Get the current, fully processed value of this axis. 77 | 78 | :return: ``0`` to ``255``, ``128`` if idle/centered or bypassed. 79 | :rtype: int 80 | """ 81 | new_value = self._update() 82 | 83 | if self.bypass: 84 | return Axis.IDLE 85 | else: 86 | return new_value 87 | 88 | @property 89 | def source_value(self) -> int: 90 | """ 91 | Get the raw source input value. 92 | 93 | *(For VirtualInput sources, this property can also be set.)* 94 | 95 | :return: ``0`` to ``65535`` 96 | :rtype: int 97 | """ 98 | return self._source.value 99 | 100 | @source_value.setter 101 | def source_value(self, value: int) -> None: 102 | """Set the raw source value for a VirtualInput axis source.""" 103 | if isinstance(self._source, VirtualInput): 104 | self._source.value = value 105 | else: 106 | raise TypeError("Only VirtualInput source values can be set manually.") 107 | 108 | @property 109 | def min(self) -> int: 110 | """ 111 | Get the configured minimum raw ``analogio`` input value. 112 | 113 | :return: ``0`` to ``65535`` 114 | :rtype: int 115 | """ 116 | return self._min 117 | 118 | @property 119 | def max(self) -> int: 120 | """ 121 | Get the configured maximum raw ``analogio`` input value. 122 | 123 | :return: ``0`` to ``65535`` 124 | :rtype: int 125 | """ 126 | return self._max 127 | 128 | @property 129 | def deadband(self) -> int: 130 | """ 131 | Get the raw, absolute value of the configured deadband. 132 | 133 | :return: ``0`` to ``65535`` 134 | :rtype: int 135 | """ 136 | return self._deadband 137 | 138 | @property 139 | def invert(self) -> bool: 140 | """ 141 | Return ``True`` if the raw `analogio` input value is inverted. 142 | 143 | :return: ``True`` if inverted, ``False`` otherwise 144 | :rtype: bool 145 | """ 146 | return self._invert 147 | 148 | def __init__( 149 | self, 150 | source=None, 151 | deadband: int = 0, 152 | min: int = 0, 153 | max: int = 65535, 154 | invert: bool = False, 155 | bypass: bool = False, 156 | ) -> None: 157 | """ 158 | Provide data source storage and scaling/deadband processing for an axis input. 159 | 160 | :param source: CircuitPython pin identifier (i.e. ``board.A0``) or any object 161 | with an int ``.value`` attribute. (Defaults to ``None``, which will create 162 | a ``VirtualInput`` source instead.) 163 | :type source: Any, optional 164 | :param deadband: Raw, absolute value of the deadband to apply around the 165 | midpoint of the raw source value. The deadband is used to prevent an axis 166 | from registering minimal values when it is centered. Setting the deadband 167 | value to ``250`` means raw input values +/- 250 from the midpoint will all 168 | be treated as the midpoint. (defaults to ``0``) 169 | :type deadband: int, optional 170 | :param min: The raw input value that corresponds to a scaled axis value of 0. 171 | Any raw input value <= to this value will get scaled to 0. Useful if the 172 | component used to generate the raw input never actually reaches 0. 173 | (defaults to ``0``) 174 | :type min: int, optional 175 | :param max: The raw input value that corresponds to a scaled axis value of 255. 176 | Any raw input value >= to this value will get scaled to 255. Useful if the 177 | component used to generate the raw input never actually reaches 65535. 178 | (defaults to ``65535``) 179 | :type max: int, optional 180 | :param invert: Set to ``True`` to invert the scaled axis value. Useful if the 181 | physical orientation of the component used to generate the raw axis input 182 | does not match the logical direction of the axis input. 183 | (defaults to ``False``) 184 | :type invert: bool, optional 185 | :param bypass: Set to ``True`` to make the axis always appear ``centered`` 186 | in USB HID reports back to the host device. (Defaults to ``False``) 187 | :type bypass: bool, optional 188 | """ 189 | self._source = Axis._initialize_source(source) 190 | self._deadband = deadband 191 | self._min = min 192 | self._max = max 193 | self._invert = invert 194 | self._value = Axis.IDLE 195 | self._last_source_value = Axis.IDLE 196 | 197 | self.bypass = bypass 198 | """Set to ``True`` to make the axis always appear idle/centered.""" 199 | 200 | # calculate raw input midpoint and scaled deadband range 201 | self._raw_midpoint = self._min + ((self._max - self._min) // 2) 202 | self._db_range = self._max - self._min - (self._deadband * 2) + 1 203 | 204 | self._update() 205 | 206 | @staticmethod 207 | def _initialize_source(source): 208 | """ 209 | Configure a source as an on-board pin, off-board input or VirtualInput. 210 | 211 | :param source: CircuitPython pin identifier (i.e. ``board.A3``), any object 212 | with an int ``.value`` attribute or a ``VirtualInput`` object. 213 | :type source: Any 214 | :return: A fully configured analog source pin or virtual input. 215 | :rtype: AnalogIn or VirtualInput 216 | """ 217 | if source is None: 218 | return VirtualInput(value=32768) 219 | elif isinstance(source, Pin): 220 | return AnalogIn(source) 221 | elif hasattr(source, "value") and isinstance(source.value, int): 222 | return source 223 | else: 224 | raise TypeError("Incompatible axis source specified.") 225 | 226 | def _update(self) -> int: 227 | """ 228 | Read raw input data and convert it to a joystick-compatible value. 229 | 230 | :return: ``0`` to ``255``, ``128`` if idle/centered. 231 | :rtype: int 232 | """ 233 | source_value = self._source.value 234 | 235 | # short-circuit processing if the source value hasn't changed 236 | if source_value == self._last_source_value: 237 | return self._value 238 | 239 | self._last_source_value = source_value 240 | 241 | # clamp raw input value to specified min/max 242 | new_value = min(max(source_value, self._min), self._max) 243 | 244 | # account for deadband 245 | if new_value < (self._raw_midpoint - self._deadband): 246 | new_value -= self._min 247 | elif new_value > (self._raw_midpoint + self._deadband): 248 | new_value = new_value - self._min - (self._deadband * 2) 249 | else: 250 | new_value = self._db_range // 2 251 | 252 | # calculate scaled joystick-compatible value and clamp to 0-255 253 | new_value = min(new_value * 256 // self._db_range, 255) 254 | 255 | # invert the axis if necessary 256 | if self._invert: 257 | self._value = 255 - new_value 258 | else: 259 | self._value = new_value 260 | 261 | return self._value 262 | 263 | 264 | class Button: 265 | """Data source storage and value processing for a button input.""" 266 | 267 | @property 268 | def value(self) -> bool: 269 | """ 270 | Get the current, fully processed value of this button input. 271 | 272 | .. warning:: 273 | 274 | Accessing this property also updates the ``.was_pressed`` and 275 | ``.was_released`` logic, which means accessing ``.value`` directly anywhere 276 | other than in a call to ``Joystick.update_button()`` can make those 277 | properties unreliable. If you need to read the current state of a button 278 | anywhere else in your input processing loop, you should be using 279 | ``.is_pressed`` or ``.is_released`` rather than ``.value``. 280 | 281 | :return: ``True`` if pressed, ``False`` if released or bypassed. 282 | :rtype: bool 283 | """ 284 | self._last_state = self._state 285 | self._state = self._source.value != self._active_low 286 | 287 | return self._state and not self.bypass 288 | 289 | @property 290 | def is_pressed(self) -> bool: 291 | """ 292 | Determine if this button is currently in the ``pressed`` state. 293 | 294 | :return: ``True`` if button is pressed, otherwise ``False`` 295 | :rtype: bool 296 | """ 297 | return self._source.value != self._active_low 298 | 299 | @property 300 | def is_released(self) -> bool: 301 | """ 302 | Determine if this button is currently in the ``released`` state. 303 | 304 | :return: ``True`` if button is released, otherwise ``False``. 305 | :rtype: bool 306 | """ 307 | return self._source.value == self._active_low 308 | 309 | @property 310 | def was_pressed(self) -> bool: 311 | """ 312 | Determine if this button was just pressed. 313 | 314 | Specifically, if the button state changed from ``released`` to ``pressed`` 315 | between the last two reads of ``Button.value``. 316 | 317 | .. warning:: 318 | 319 | This property only works reliably when ``Button.value`` is accessed *once 320 | per iteration of your input processing loop*. If your code uses the 321 | built-in ``Joystick.add_input()`` method and associated input lists along 322 | with a single call to ``Joystick.update()``, you should be fine. 323 | 324 | :return: ``True`` if the button was just pressed, ``False`` otherwise. 325 | :rtype: bool 326 | """ 327 | return self._state is True and self._last_state is False 328 | 329 | @property 330 | def was_released(self) -> bool: 331 | """ 332 | Determine if this button was just released. 333 | 334 | Specifically, if the button state changed from ``pressed`` to ``released`` 335 | between the last two reads of ``Button.value``. 336 | 337 | .. warning:: 338 | 339 | This property only works reliably when ``Button.value`` is accessed *once 340 | per iteration of your input processing loop*. If your code uses the 341 | built-in ``Joystick.add_input()`` method and associated input lists along 342 | with a single call to ``Joystick.update()``, you should be fine. 343 | 344 | :return: ``True`` if the button was just released, ``False`` otherwise. 345 | :rtype: bool 346 | """ 347 | return self._state is False and self._last_state is True 348 | 349 | @property 350 | def source_value(self) -> bool: 351 | """ 352 | Get the raw source input value. 353 | 354 | *(For VirtualInput sources, this property can also be set.)* 355 | 356 | :return: ``True`` or ``False`` 357 | :rtype: bool 358 | """ 359 | return self._source.value is True 360 | 361 | @source_value.setter 362 | def source_value(self, value: bool) -> None: 363 | """Set the raw source value for a VirtualInput button source.""" 364 | if not isinstance(self._source, VirtualInput): 365 | raise TypeError("Only VirtualInput source values can be set manually.") 366 | self._source.value = value 367 | 368 | @property 369 | def active_low(self) -> bool: 370 | """ 371 | Get the input configuration state of the button. 372 | 373 | :return: ``True`` if the button is active low, ``False`` otherwise. 374 | :rtype: bool 375 | """ 376 | return self._active_low 377 | 378 | def __init__( 379 | self, 380 | source=None, 381 | active_low: bool = True, 382 | bypass: bool = False, 383 | ) -> None: 384 | """ 385 | Provide data source storage and value processing for a button input. 386 | 387 | :param source: CircuitPython pin identifier (i.e. ``board.D2``), or any object 388 | with a boolean ``.value`` attribute. (Defaults to ``None``, which will 389 | create a ``VirtualInput`` source instead.) 390 | :type source: Any, optional 391 | :param active_low: Set to ``True`` if the input pin is active low 392 | (reads ``False`` when the button is pressed), otherwise set to ``False``. 393 | (defaults to ``True``) 394 | :type active_low: bool, optional 395 | :param bypass: Set to ``True`` to make the button always appear ``released`` 396 | in USB HID reports back to the host device. (Defaults to ``False``) 397 | :type bypass: bool, optional 398 | """ 399 | self._source = Button._initialize_source(source, active_low) 400 | self._active_low = active_low 401 | self._state = False 402 | self._last_state = False 403 | 404 | self.bypass = bypass 405 | """Set to ``True`` to make the button always appear ``released``.""" 406 | 407 | @staticmethod 408 | def _initialize_source(source, active_low: bool): 409 | """ 410 | Configure a source as an on-board pin, off-board input or VirtualInput. 411 | 412 | :param source: CircuitPython pin identifier (i.e. ``board.D2``), any object 413 | with a boolean ``.value`` attribute or a ``VirtualInput`` object. 414 | :type source: Any 415 | :param active_low: Set to ``True`` if the input pin is active low (reads 416 | ``False`` when the button is pressed), otherwise set to ``False``. 417 | :type active_low: bool 418 | :return: A fully configured digital source pin or virtual input. 419 | :rtype: Any 420 | """ 421 | if source is None: 422 | return VirtualInput(value=active_low) 423 | elif isinstance(source, Pin): 424 | source_gpio = DigitalInOut(source) 425 | source_gpio.direction = Direction.INPUT 426 | if active_low: 427 | source_gpio.pull = Pull.UP 428 | else: 429 | source_gpio.pull = Pull.DOWN 430 | return source_gpio 431 | elif hasattr(source, "value") and isinstance(source.value, bool): 432 | return source 433 | else: 434 | raise TypeError("Incompatible button source specified.") 435 | 436 | 437 | class Hat: 438 | """Data source storage and value conversion for hat switch inputs.""" 439 | 440 | U = 0 441 | """Alias for the ``UP`` switch position.""" 442 | 443 | UR = 1 444 | """Alias for the ``UP + RIGHT`` switch position.""" 445 | 446 | R = 2 447 | """Alias for the ``RIGHT`` switch position.""" 448 | 449 | DR = 3 450 | """Alias for the ``DOWN + RIGHT`` switch position.""" 451 | 452 | D = 4 453 | """Alias for the ``DOWN`` switch position.""" 454 | 455 | DL = 5 456 | """Alias for the ``DOWN + LEFT`` switch position.""" 457 | 458 | L = 6 459 | """Alias for the ``LEFT`` switch position.""" 460 | 461 | UL = 7 462 | """Alias for the ``UP + LEFT`` switch position.""" 463 | 464 | IDLE = 8 465 | """Alias for the ``IDLE`` switch position.""" 466 | 467 | @property 468 | def value(self) -> int: 469 | """ 470 | Get the current, fully processed value of this hat switch. 471 | 472 | :return: Current position value (always ``IDLE`` if bypassed), as follows: 473 | 474 | * ``0`` = UP 475 | * ``1`` = UP + RIGHT 476 | * ``2`` = RIGHT 477 | * ``3`` = DOWN + RIGHT 478 | * ``4`` = DOWN 479 | * ``5`` = DOWN + LEFT 480 | * ``6`` = LEFT 481 | * ``7`` = UP + LEFT 482 | * ``8`` = IDLE 483 | 484 | :rtype: int 485 | """ 486 | new_value = self._update() 487 | 488 | if self.bypass: 489 | return Hat.IDLE 490 | else: 491 | return new_value 492 | 493 | @property 494 | def packed_source_values(self) -> int: 495 | """ 496 | Get the current packed value of all four button input source values. 497 | 498 | :return: Packed button input source values in one byte (``0000RLDU``). 499 | :rtype: int 500 | """ 501 | pv = self.up.source_value 502 | pv |= self.down.source_value << 1 503 | pv |= self.left.source_value << 2 504 | pv |= self.right.source_value << 3 505 | return pv 506 | 507 | @property 508 | def active_low(self) -> bool: 509 | """ 510 | Get the input configuration state of the hat switch buttons. 511 | 512 | :return: ``True`` if the buttons are active low, ``False`` otherwise. 513 | :rtype: bool 514 | """ 515 | return self._active_low 516 | 517 | def __init__( 518 | self, 519 | up=None, 520 | down=None, 521 | left=None, 522 | right=None, 523 | active_low: bool = True, 524 | bypass: bool = False, 525 | ) -> None: 526 | """ 527 | Provide data source storage and value processing for a hat switch input. 528 | 529 | :param up: CircuitPython pin identifier (i.e. ``board.D2``) or any object with 530 | a boolean ``.value`` attribute. (Defaults to ``None``, which will create 531 | a ``VirtualInput`` source instead.) 532 | :type up: Any, optional 533 | :param down: CircuitPython pin identifier (i.e. ``board.D2``) or any object with 534 | a boolean ``.value`` attribute. (Defaults to ``None``, which will create 535 | a ``VirtualInput`` source instead.) 536 | :type down: Any, optional 537 | :param left: CircuitPython pin identifier (i.e. ``board.D2``) or any object with 538 | a boolean ``.value`` attribute. (Defaults to ``None``, which will create 539 | a ``VirtualInput`` source instead.) 540 | :type left: Any, optional 541 | :param right: CircuitPython pin identifier (i.e. ``board.D2``) or any object 542 | with a boolean ``.value`` attribute. (Defaults to ``None``, which will 543 | create a ``VirtualInput`` source instead.) 544 | :type right: Any, optional 545 | :param active_low: Set to ``True`` if the input pins are active low 546 | (read ``False`` when buttons are pressed), otherwise set to ``False``. 547 | (defaults to ``True``) 548 | :type active_low: bool, optional 549 | :param bypass: Set to ``True`` to make the hat switch always appear ``idle`` 550 | in USB HID reports back to the host device. (Defaults to ``False``) 551 | :type bypass: bool, optional 552 | """ 553 | self.up = Button(up, active_low) 554 | """Button object associated with the ``up`` input.""" 555 | 556 | self.down = Button(down, active_low) 557 | """Button object associated with the ``down`` input.""" 558 | 559 | self.left = Button(left, active_low) 560 | """Button object associated with the ``left`` input.""" 561 | 562 | self.right = Button(right, active_low) 563 | """Button object associated with the ``right`` input.""" 564 | 565 | self._active_low = active_low 566 | self._value = Hat.IDLE 567 | 568 | self.bypass = bypass 569 | """Set to ``True`` to make the hat switch always appear ``idle``.""" 570 | 571 | self._update() 572 | 573 | def unpack_source_values(self, source_values: int) -> None: 574 | """ 575 | Unpack all four source values from a single packed integer. 576 | 577 | :param source_values: Packed button source values in one byte (``0000RLDU``). 578 | :type source_values: int 579 | 580 | .. note:: 581 | 582 | This operation is only valid for hat switches composed of 583 | ``VirtualInput`` objects. 584 | """ 585 | self.up.source_value = (source_values & 0x01) == 0x01 586 | self.down.source_value = ((source_values >> 1) & 0x01) == 0x01 587 | self.left.source_value = ((source_values >> 2) & 0x01) == 0x01 588 | self.right.source_value = ((source_values >> 3) & 0x01) == 0x01 589 | 590 | def _update(self) -> int: 591 | """Update the angular position value based on discrete input states.""" 592 | U = self.up.value 593 | D = self.down.value 594 | L = self.left.value 595 | R = self.right.value 596 | 597 | if U and R: 598 | self._value = Hat.UR 599 | elif U and L: 600 | self._value = Hat.UL 601 | elif U: 602 | self._value = Hat.U 603 | elif D and R: 604 | self._value = Hat.DR 605 | elif D and L: 606 | self._value = Hat.DL 607 | elif D: 608 | self._value = Hat.D 609 | elif L: 610 | self._value = Hat.L 611 | elif R: 612 | self._value = Hat.R 613 | else: 614 | self._value = Hat.IDLE 615 | return self._value 616 | -------------------------------------------------------------------------------- /joystick_xl/joystick.py: -------------------------------------------------------------------------------- 1 | """ 2 | The base JoystickXL class for updating input states and sending USB HID reports. 3 | 4 | This module provides the necessary functions to create a JoystickXL object, 5 | retrieve its input counts, associate input objects and update its input states. 6 | """ 7 | 8 | import struct 9 | import time 10 | 11 | # These typing imports help during development in vscode but fail in CircuitPython 12 | try: 13 | from typing import Tuple, Union 14 | except ImportError: 15 | pass 16 | 17 | from joystick_xl.hid import _get_device 18 | from joystick_xl.inputs import Axis, Button, Hat 19 | 20 | 21 | class Joystick: 22 | """Base JoystickXL class for updating input states and sending USB HID reports.""" 23 | 24 | _num_axes = 0 25 | """The number of axes this joystick can support.""" 26 | 27 | _num_buttons = 0 28 | """The number of buttons this joystick can support.""" 29 | 30 | _num_hats = 0 31 | """The number of hat switches this joystick can support.""" 32 | 33 | _report_size = 0 34 | """The size (in bytes) of USB HID reports for this joystick.""" 35 | 36 | @property 37 | def num_axes(self) -> int: 38 | """Return the number of available axes in the USB HID descriptor.""" 39 | return self._num_axes 40 | 41 | @property 42 | def num_buttons(self) -> int: 43 | """Return the number of available buttons in the USB HID descriptor.""" 44 | return self._num_buttons 45 | 46 | @property 47 | def num_hats(self) -> int: 48 | """Return the number of available hat switches in the USB HID descriptor.""" 49 | return self._num_hats 50 | 51 | def __init__(self) -> None: 52 | """ 53 | Create a JoystickXL object with all inputs in idle states. 54 | 55 | .. code:: 56 | 57 | from joystick_xl.joystick import Joystick 58 | 59 | js = Joystick() 60 | 61 | .. note:: A JoystickXL ``usb_hid.Device`` object has to be created in 62 | ``boot.py`` before creating a ``Joystick()`` object in ``code.py``, 63 | otherwise an exception will be thrown. 64 | """ 65 | # load configuration from ``boot_out.txt`` 66 | try: 67 | with open("/boot_out.txt", "r") as boot_out: 68 | for line in boot_out.readlines(): 69 | if "JoystickXL" in line: 70 | config = [int(s) for s in line.split() if s.isdigit()] 71 | if len(config) < 4: 72 | raise (ValueError) 73 | Joystick._num_axes = config[0] 74 | Joystick._num_buttons = config[1] 75 | Joystick._num_hats = config[2] 76 | Joystick._report_size = config[3] 77 | break 78 | if Joystick._report_size == 0: 79 | raise (ValueError) 80 | except (OSError, ValueError): 81 | raise (Exception("Error loading JoystickXL configuration.")) 82 | 83 | self._device = _get_device() 84 | self._report = bytearray(self._report_size) 85 | self._last_report = bytearray(self._report_size) 86 | self._format = "<" 87 | 88 | self.axis = list() 89 | """List of axis inputs associated with this joystick through ``add_input``.""" 90 | 91 | self._axis_states = list() 92 | for _ in range(self.num_axes): 93 | self._axis_states.append(Axis.IDLE) 94 | self._format += "B" 95 | 96 | self.hat = list() 97 | """List of hat inputs associated with this joystick through ``add_input``.""" 98 | 99 | self._hat_states = [Hat.IDLE] * self.num_hats 100 | if self.num_hats > 2: 101 | self._format += "H" 102 | elif self.num_hats: 103 | self._format += "B" 104 | 105 | self.button = list() 106 | """List of button inputs associated with this joystick through ``add_input``.""" 107 | 108 | self._button_states = list() 109 | for _ in range((self.num_buttons // 8) + bool(self.num_buttons % 8)): 110 | self._button_states.append(0) 111 | self._format += "B" 112 | 113 | try: 114 | self.reset_all() 115 | except OSError: 116 | time.sleep(1) 117 | self.reset_all() 118 | 119 | @staticmethod 120 | def _validate_axis_value(axis: int, value: int) -> bool: 121 | """ 122 | Ensure the supplied axis index and value are valid. 123 | 124 | :param axis: The 0-based index of the axis to validate. 125 | :type axis: int 126 | :param value: The axis value to validate. 127 | :type value: int 128 | :raises ValueError: No axes are configured for the JoystickXL device. 129 | :raises ValueError: The supplied axis index is out of range. 130 | :raises ValueError: The supplied axis value is out of range. 131 | :return: ``True`` if the supplied axis index and value are valid. 132 | :rtype: bool 133 | """ 134 | if Joystick._num_axes == 0: 135 | raise ValueError("There are no axes configured.") 136 | if axis + 1 > Joystick._num_axes: 137 | raise ValueError("Specified axis is out of range.") 138 | if not Axis.MIN <= value <= Axis.MAX: 139 | raise ValueError("Axis value must be in range 0 to 255") 140 | return True 141 | 142 | @staticmethod 143 | def _validate_button_number(button: int) -> bool: 144 | """ 145 | Ensure the supplied button index is valid. 146 | 147 | :param button: The 0-based index of the button to validate. 148 | :type button: int 149 | :raises ValueError: No buttons are configured for the JoystickXL device. 150 | :raises ValueError: The supplied button index is out of range. 151 | :return: ``True`` if the supplied button index is valid. 152 | :rtype: bool 153 | """ 154 | if Joystick._num_buttons == 0: 155 | raise ValueError("There are no buttons configured.") 156 | if not 0 <= button <= Joystick._num_buttons - 1: 157 | raise ValueError("Specified button is out of range.") 158 | return True 159 | 160 | @staticmethod 161 | def _validate_hat_value(hat: int, position: int) -> bool: 162 | """ 163 | Ensure the supplied hat switch index and position are valid. 164 | 165 | :param hat: The 0-based index of the hat switch to validate. 166 | :type hat: int 167 | :param value: The hat switch position to validate. 168 | :type value: int 169 | :raises ValueError: No hat switches are configured for the JoystickXL device. 170 | :raises ValueError: The supplied hat switch index is out of range. 171 | :raises ValueError: The supplied hat switch position is out of range. 172 | :return: ``True`` if the supplied hat switch index and position are valid. 173 | :rtype: bool 174 | """ 175 | if Joystick._num_hats == 0: 176 | raise ValueError("There are no hat switches configured.") 177 | if hat + 1 > Joystick._num_hats: 178 | raise ValueError("Specified hat is out of range.") 179 | if not 0 <= position <= 8: 180 | raise ValueError("Hat value must be in range 0 to 8") 181 | return True 182 | 183 | def add_input(self, *input: Union[Axis, Button, Hat]) -> None: 184 | """ 185 | Associate one or more axis, button or hat inputs with the joystick. 186 | 187 | The provided input(s) are automatically added to the ``axis``, ``button`` and 188 | ``hat`` lists based on their type. The order in which inputs are added will 189 | determine their index/reference number. (i.e., the first button object that is 190 | added will be ``Joystick.button[0]``.) Inputs of all types can be added at the 191 | same time and will be sorted into the correct list. 192 | 193 | :param input: One or more ``Axis``, ``Button`` or ``Hat`` objects. 194 | :type input: Axis, Button, or Hat 195 | :raises TypeError: If an object that is not an ``Axis``, ``Button`` or ``Hat`` 196 | is passed in. 197 | :raises OverflowError: If an attempt is made to add more than the available 198 | number of axes, buttons or hat switches to the respective list. 199 | """ 200 | for i in input: 201 | if isinstance(i, Axis): 202 | if len(self.axis) < self._num_axes: 203 | self.axis.append(i) 204 | else: 205 | raise OverflowError("List is full, cannot add another axis.") 206 | elif isinstance(i, Button): 207 | if len(self.button) < self._num_buttons: 208 | self.button.append(i) 209 | else: 210 | raise OverflowError("List is full, cannot add another button.") 211 | elif isinstance(i, Hat): 212 | if len(self.hat) < self._num_hats: 213 | self.hat.append(i) 214 | else: 215 | raise OverflowError("List is full, cannot add another hat switch.") 216 | else: 217 | raise TypeError("Input must be a Button, Axis or Hat object.") 218 | 219 | def update(self, always: bool = False, halt_on_error: bool = False) -> None: 220 | """ 221 | Update all inputs in associated input lists and generate a USB HID report. 222 | 223 | :param always: When ``True``, send a report even if it is identical to the last 224 | report that was sent out. Defaults to ``False``. 225 | :type always: bool, optional 226 | :param halt_on_error: When ``True``, an exception will be raised and the program 227 | will halt if an ``OSError`` occurs when the report is sent. When ``False``, 228 | the report will simply be dropped and no exception will be raised. Defaults 229 | to ``False``. 230 | :type halt_on_error: bool, optional 231 | """ 232 | # Update axis values but defer USB HID report generation. 233 | if len(self.axis): 234 | axis_values = [(i, a.value) for i, a in enumerate(self.axis)] 235 | self.update_axis(*axis_values, defer=True, skip_validation=True) 236 | 237 | # Update button states but defer USB HID report generation. 238 | if len(self.button): 239 | button_values = [(i, b.value) for i, b in enumerate(self.button)] 240 | self.update_button(*button_values, defer=True, skip_validation=True) 241 | 242 | # Update hat switch values, but defer USB HID report generation. 243 | if len(self.hat): 244 | hat_values = [(i, h.value) for i, h in enumerate(self.hat)] 245 | self.update_hat(*hat_values, defer=True, skip_validation=True) 246 | 247 | # Generate a USB HID report. 248 | report_data = list() 249 | 250 | report_data.extend(self._axis_states) 251 | 252 | if self.num_hats: 253 | _hat_state = 0 254 | for i in range(self.num_hats): 255 | _hat_state |= self._hat_states[i] << (4 * (self.num_hats - i - 1)) 256 | report_data.append(_hat_state) 257 | 258 | report_data.extend(self._button_states) 259 | 260 | struct.pack_into(self._format, self._report, 0, *report_data) 261 | 262 | # Send the USB HID report if required. 263 | if always or self._last_report != self._report: 264 | try: 265 | self._device.send_report(self._report) 266 | self._last_report[:] = self._report 267 | except OSError: 268 | # This can occur if the USB is busy, or the host never properly 269 | # connected to the USB device. We just drop the update and try later. 270 | if halt_on_error: 271 | raise 272 | 273 | def reset_all(self) -> None: 274 | """Reset all inputs to their idle states.""" 275 | for i in range(self.num_axes): 276 | self._axis_states[i] = Axis.IDLE 277 | for i in range(len(self._button_states)): 278 | self._button_states[i] = 0 279 | for i in range(self.num_hats): 280 | self._hat_states[i] = Hat.IDLE 281 | self.update(always=True) 282 | 283 | def update_axis( 284 | self, 285 | *axis: Tuple[int, int], 286 | defer: bool = False, 287 | skip_validation: bool = False, 288 | ) -> None: 289 | """ 290 | Update the value of one or more axis input(s). 291 | 292 | :param axis: One or more tuples containing an axis index (0-based) and value 293 | (``0`` to ``255``, with ``128`` indicating the axis is idle/centered). 294 | :type axis: Tuple[int, int] 295 | :param defer: When ``True``, prevents sending a USB HID report upon completion. 296 | Defaults to ``False``. 297 | :type defer: bool 298 | :param skip_validation: When ``True``, bypasses the normal input number/value 299 | validation that occurs before they get processed. This is used for *known 300 | good values* that are generated using the ``Joystick.axis[]``, 301 | ``Joystick.button[]`` and ``Joystick.hat[]`` lists. Defaults to ``False``. 302 | :type skip_validation: bool 303 | 304 | .. code:: 305 | 306 | # Updates a single axis 307 | update_axis((0, 42)) # 0 = x-axis 308 | 309 | # Updates multiple axes 310 | update_axis((1, 22), (3, 237)) # 1 = y-axis, 3 = rx-axis 311 | 312 | .. note:: 313 | 314 | ``update_axis`` is called automatically for any axis objects added to the 315 | built in ``Joystick.axis[]`` list when ``Joystick.update()`` is called. 316 | """ 317 | for a, value in axis: 318 | if skip_validation or self._validate_axis_value(a, value): 319 | if self.num_axes > 7 and a > 5: 320 | a = self.num_axes - a + 5 # reverse sequence for sliders 321 | self._axis_states[a] = value 322 | 323 | if not defer: 324 | self.update() 325 | 326 | def update_button( 327 | self, 328 | *button: Tuple[int, bool], 329 | defer: bool = False, 330 | skip_validation: bool = False, 331 | ) -> None: 332 | """ 333 | Update the value of one or more button input(s). 334 | 335 | :param button: One or more tuples containing a button index (0-based) and 336 | value (``True`` if pressed, ``False`` if released). 337 | :type button: Tuple[int, bool] 338 | :param defer: When ``True``, prevents sending a USB HID report upon completion. 339 | Defaults to ``False``. 340 | :type defer: bool 341 | :param skip_validation: When ``True``, bypasses the normal input number/value 342 | validation that occurs before they get processed. This is used for *known 343 | good values* that are generated using the ``Joystick.axis[]``, 344 | ``Joystick.button[]`` and ``Joystick.hat[]`` lists. Defaults to ``False``. 345 | :type skip_validation: bool 346 | 347 | .. code:: 348 | 349 | # Update a single button 350 | update_button((0, True)) # 0 = b1 351 | 352 | # Updates multiple buttons 353 | update_button((1, False), (7, True)) # 1 = b2, 7 = b8 354 | 355 | .. note:: 356 | 357 | ``update_button`` is called automatically for any button objects added to the 358 | built in ``Joystick.button[]`` list when ``Joystick.update()`` is called. 359 | """ 360 | for b, value in button: 361 | if skip_validation or self._validate_button_number(b): 362 | _bank = b // 8 363 | _bit = b % 8 364 | if value: 365 | self._button_states[_bank] |= 1 << _bit 366 | else: 367 | self._button_states[_bank] &= ~(1 << _bit) 368 | if not defer: 369 | self.update() 370 | 371 | def update_hat( 372 | self, 373 | *hat: Tuple[int, int], 374 | defer: bool = False, 375 | skip_validation: bool = False, 376 | ) -> None: 377 | """ 378 | Update the value of one or more hat switch input(s). 379 | 380 | :param hat: One or more tuples containing a hat switch index (0-based) and 381 | value. Valid hat switch values range from ``0`` to ``8`` as follows: 382 | 383 | * ``0`` = UP 384 | * ``1`` = UP + RIGHT 385 | * ``2`` = RIGHT 386 | * ``3`` = DOWN + RIGHT 387 | * ``4`` = DOWN 388 | * ``5`` = DOWN + LEFT 389 | * ``6`` = LEFT 390 | * ``7`` = UP + LEFT 391 | * ``8`` = IDLE 392 | 393 | :type hat: Tuple[int, int] 394 | :param defer: When ``True``, prevents sending a USB HID report upon completion. 395 | Defaults to ``False``. 396 | :type defer: bool 397 | :param skip_validation: When ``True``, bypasses the normal input number/value 398 | validation that occurs before they get processed. This is used for *known 399 | good values* that are generated using the ``Joystick.axis[]``, 400 | ``Joystick.button[]`` and ``Joystick.hat[]`` lists. Defaults to ``False``. 401 | :type skip_validation: bool 402 | 403 | .. code:: 404 | 405 | # Updates a single hat switch 406 | update_hat((0, 3)) # 0 = h1 407 | 408 | # Updates multiple hat switches 409 | update_hat((1, 8), (3, 1)) # 1 = h2, 3 = h4 410 | 411 | .. note:: 412 | 413 | ``update_hat`` is called automatically for any hat switch objects added to 414 | the built in ``Joystick.hat[]`` list when ``Joystick.update()`` is called. 415 | """ 416 | for h, value in hat: 417 | if skip_validation or self._validate_hat_value(h, value): 418 | self._hat_states[h] = value 419 | if not defer: 420 | self.update() 421 | -------------------------------------------------------------------------------- /joystick_xl/tools.py: -------------------------------------------------------------------------------- 1 | """Tools to assist in the development and general use of JoystickXL.""" 2 | 3 | import time 4 | 5 | # These are all CircuitPython built-ins 6 | import board # type: ignore 7 | import digitalio # type: ignore 8 | from microcontroller import Pin # type: ignore 9 | from supervisor import runtime # type: ignore 10 | 11 | from joystick_xl import __version__ 12 | from joystick_xl.inputs import Axis, Hat, VirtualInput 13 | from joystick_xl.joystick import Joystick 14 | 15 | 16 | def TestAxes(js: Joystick, step: int = 5, quiet: bool = False) -> None: 17 | """ 18 | Exercise each axis in the supplied ``Joystick`` object. 19 | 20 | :param js: The ``Joystick`` object to test. 21 | :type js: Joystick 22 | :param step: The size of axis adjustment steps. A higher value provides coarser 23 | axis movement and faster tests. Set to ``1`` to test every possible axis value 24 | (which can result in long test runs). (Default is 5) 25 | :type step: int, optional 26 | :param quiet: Set to ``True`` to disable console output during testing. (Defaults 27 | to ``False``) 28 | :type quiet: bool, optional 29 | """ 30 | if not js.num_axes: 31 | if not quiet: 32 | print("> No axis inputs configured! (check boot.py)") 33 | return 34 | if not quiet: 35 | print("> Testing axes...", end="") 36 | for a in range(js.num_axes): 37 | for i in range(Axis.IDLE, Axis.MIN, -step): 38 | js.update_axis((a, i)) 39 | for i in range(Axis.MIN, Axis.MAX, step): 40 | js.update_axis((a, i)) 41 | for i in range(Axis.MAX, Axis.IDLE - 1, -step): 42 | js.update_axis((a, i)) 43 | js.update_axis((a, Axis.IDLE)) 44 | if not quiet: 45 | print("DONE") 46 | 47 | 48 | def TestButtons(js: Joystick, pace: float = 0.05, quiet: bool = False) -> None: 49 | """ 50 | Exercise each button in the supplied ``Joystick`` object. 51 | 52 | :param js: The ``Joystick`` object to test. 53 | :type js: Joystick 54 | :param pace: Duration (in seconds) and time between button presses. (Default is 55 | 0.05 seconds) 56 | :type pace: float, optional 57 | :param quiet: Set to ``True`` to disable console output during testing. (Defaults 58 | to ``False``.) 59 | :type quiet: bool, optional 60 | """ 61 | if not js.num_buttons: 62 | if not quiet: 63 | print("> No button inputs configured! (check boot.py)") 64 | return 65 | if not quiet: 66 | print("> Testing buttons...", end="") 67 | for i in range(js.num_buttons): 68 | js.update_button((i, True)) 69 | time.sleep(pace) 70 | js.update_button((i, False)) 71 | time.sleep(pace) 72 | if not quiet: 73 | print("DONE") 74 | 75 | 76 | def TestHats(js: Joystick, pace: float = 0.25, quiet: bool = False) -> None: 77 | """ 78 | Exercise each hat switch in the supplied ``Joystick`` object. 79 | 80 | :param js: The ``Joystick`` object to test. 81 | :type js: Joystick 82 | :param pace: Duration (in seconds) that each hat switch direction will be engaged 83 | for. (Default is 0.25 seconds) 84 | :type pace: float, optional 85 | :param quiet: Set to ``True`` to disable console output during testing. (Defaults 86 | to ``False``.) 87 | :type quiet: bool, optional 88 | """ 89 | if not js.num_hats: 90 | if not quiet: 91 | print("> No hat switch inputs configured! (check boot.py)") 92 | return 93 | if not quiet: 94 | print("> Testing hat switches...", end="") 95 | for h in range(js.num_hats): 96 | for i in range(Hat.U, Hat.IDLE + 1): 97 | js.update_hat((h, i)) 98 | time.sleep(pace) 99 | if not quiet: 100 | print("DONE") 101 | 102 | 103 | def TestConsole(button_pin: Pin = None): 104 | """ 105 | Run JoystickXL's REPL-based, built-in test console. 106 | 107 | :param button_pin: Specify the pin to use as TestConsole's test button. Defaults 108 | to ``board.D2`` (``board.GP2`` on RP2040-based devices). 109 | :type button_pin: microcontroller.Pin, optional 110 | """ 111 | INVALID_OPERATION = "> Invalid operation." 112 | 113 | js = Joystick() 114 | last_cmd = "" 115 | si = 1 # start index 116 | pt = 0.25 # press time 117 | 118 | def ValidateIndex(i: int, limit: int, name: str) -> int: 119 | if not limit: 120 | print("> No", name, "inputs configured! (check boot.py)") 121 | return -1 122 | if i < si or i > limit - (1 - si): 123 | print("> Invalid", name, "specified.") 124 | return -1 125 | else: 126 | return i - si 127 | 128 | def MoveAxis(axis: int, stop: int, step: int) -> None: 129 | for i in range(Axis.IDLE, stop, step): 130 | js.update_axis((axis, i)) 131 | for i in range(stop, Axis.IDLE, -step): 132 | js.update_axis((axis, i)) 133 | js.update_axis((axis, Axis.IDLE)) 134 | 135 | print("\nJoystickXL", __version__, "- Test Console\n") 136 | print("Using 1-based indexing.") 137 | print("Button Clicks = 0.25s") 138 | print("Test Button = ", end="") 139 | try: 140 | # Attempt to configure a test button 141 | if button_pin is None: 142 | try: 143 | # for most CircuitPython boards 144 | pin = board.D2 145 | except AttributeError: 146 | # for RP2040-based boards 147 | pin = board.GP2 148 | else: 149 | pin = button_pin 150 | button = digitalio.DigitalInOut(pin) 151 | button.direction = digitalio.Direction.INPUT 152 | button.pull = digitalio.Pull.UP 153 | print(pin) 154 | except AttributeError: 155 | # Fall back to a VirtualInput if button assignment fails 156 | button = VirtualInput(value=True) 157 | print("(Not Assigned)") 158 | 159 | print("Enter command (? for list)") 160 | 161 | while True: 162 | 163 | print(": ", end="") 164 | cmd = "" 165 | 166 | while not runtime.serial_bytes_available: 167 | if button.value is False: 168 | break 169 | 170 | if runtime.serial_bytes_available: 171 | cmd = input().lower() # prompt and user input 172 | 173 | if not cmd and last_cmd: # repeat last command if nothing was entered 174 | cmd = last_cmd 175 | print("(", cmd, ")") 176 | else: 177 | last_cmd = cmd 178 | 179 | # extract a number from the entered command (0 if no number was entered) 180 | num = int("0" + "".join(filter(lambda i: i.isdigit(), cmd))) 181 | 182 | # axis functions 183 | if cmd.startswith("a"): 184 | if cmd.endswith("t"): 185 | TestAxes(js) 186 | continue 187 | i = ValidateIndex(num, js.num_axes, "axis") 188 | if i < 0: 189 | continue 190 | elif cmd.endswith("u"): 191 | operation = "UP" 192 | value = Axis.MAX 193 | step = 3 194 | elif cmd.endswith("d"): 195 | operation = "DOWN" 196 | value = Axis.MIN 197 | step = -3 198 | else: 199 | print(INVALID_OPERATION) 200 | continue 201 | print("> Axis", i + si, operation) 202 | MoveAxis(i, value, step) 203 | 204 | # button functions 205 | elif cmd.startswith("b"): 206 | if cmd.endswith("t"): 207 | TestButtons(js) 208 | continue 209 | i = ValidateIndex(num, js.num_buttons, "button") 210 | if i < 0: 211 | continue 212 | print("> Button", i + si, "CLICK") 213 | js.update_button((i, True)) 214 | time.sleep(pt) 215 | js.update_button((i, False)) 216 | 217 | # hat functions 218 | elif cmd.startswith("h"): 219 | if cmd.endswith("t"): 220 | TestHats(js) 221 | continue 222 | i = ValidateIndex(num, js.num_hats, "hat switch") 223 | if i < 0: 224 | continue 225 | elif "u" in cmd and "l" in cmd: 226 | operation = "UP+LEFT" 227 | value = Hat.UL 228 | elif "u" in cmd and "r" in cmd: 229 | operation = "UP+RIGHT" 230 | value = Hat.UR 231 | elif "d" in cmd and "l" in cmd: 232 | operation = "DOWN+LEFT" 233 | value = Hat.DL 234 | elif "d" in cmd and "r" in cmd: 235 | operation = "DOWN+RIGHT" 236 | value = Hat.DR 237 | elif cmd.endswith("u"): 238 | operation = "UP" 239 | value = Hat.U 240 | elif cmd.endswith("d"): 241 | operation = "DOWN" 242 | value = Hat.D 243 | elif cmd.endswith("l"): 244 | operation = "LEFT" 245 | value = Hat.L 246 | elif cmd.endswith("r"): 247 | operation = "RIGHT" 248 | value = Hat.R 249 | else: 250 | print(INVALID_OPERATION) 251 | continue 252 | print("> Hat Switch", i + si, operation) 253 | js.update_hat((i, value)) 254 | time.sleep(pt) 255 | js.update_hat((i, Hat.IDLE)) 256 | 257 | # auto-test functions 258 | elif cmd.startswith("t"): 259 | if js.num_axes: 260 | TestAxes(js) 261 | if js.num_buttons: 262 | TestButtons(js) 263 | if js.num_hats: 264 | TestHats(js) 265 | print("> All tests completed.") 266 | pass 267 | 268 | # use 0-based indices 269 | elif cmd.startswith("0"): 270 | si = 0 271 | print("> Using 0-based indexing.") 272 | 273 | # use 1-based indices 274 | elif cmd.startswith("1"): 275 | si = 1 276 | print("> Using 1-based indexing.") 277 | 278 | # set button press time 279 | elif cmd.startswith("p"): 280 | pt = num / 100 281 | print("> Button presses set to", pt, "seconds.") 282 | 283 | # help 284 | elif cmd.startswith("?"): 285 | print(" a = axis (ex. `a2u`, `a1d`, `at`)") 286 | print(" b = button (ex. `b13`, `bt`)") 287 | print(" h = hat (ex. `h1u`, `h1d`, `h1ul`, `h1dr`, `ht`)") 288 | print(" t = test all") 289 | print(" 0 = 0-based indexing (button 1 = 0)") 290 | print(" 1 = 1-based indexing (button 1 = 1)") 291 | print(" p = click time (ex. `p150` = 1.5 seconds") 292 | print(" q = quit") 293 | 294 | # quit 295 | elif cmd.startswith("q"): 296 | break 297 | 298 | # unrecognized command 299 | else: 300 | print("> Unrecognized command. (? for list)") 301 | --------------------------------------------------------------------------------