Merge pull request #2832 from jedgarpark/xac-joystick
XAC joysticks first commit
This commit is contained in:
commit
ad042d2498
4 changed files with 348 additions and 0 deletions
15
TRRS_Trinkey_Demos/XAC_Joystick/XACsettings.py
Normal file
15
TRRS_Trinkey_Demos/XAC_Joystick/XACsettings.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Bill Binko
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#Change this to True to swap horizonatal and vertical axes
|
||||||
|
swapAxes = False
|
||||||
|
|
||||||
|
#Change this to True to invert (flip) the horizontal axis
|
||||||
|
invertHor = False
|
||||||
|
|
||||||
|
#Change this to True to invert (flip) the vertical axis
|
||||||
|
invertVert = True
|
||||||
|
|
||||||
|
#Increase this to make the motion smoother (with more lag)
|
||||||
|
#Decrease to make more responsive (Min=1 Default=3 Max=Any but>20 is unreasonable)
|
||||||
|
smoothingFactor = 2
|
||||||
75
TRRS_Trinkey_Demos/XAC_Joystick/boot.py
Normal file
75
TRRS_Trinkey_Demos/XAC_Joystick/boot.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Bill Binko
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import usb_midi
|
||||||
|
import usb_hid
|
||||||
|
|
||||||
|
print("In boot.py")
|
||||||
|
|
||||||
|
# storage.disable_usb_device()
|
||||||
|
|
||||||
|
# usb_cdc.enable(console=True, data=True)
|
||||||
|
|
||||||
|
usb_midi.disable()
|
||||||
|
xac_descriptor=bytes(
|
||||||
|
# This descriptor mimics the simple joystick from PDP that the XBox likes
|
||||||
|
(
|
||||||
|
0x05,
|
||||||
|
0x01, # Usage Page (Desktop),
|
||||||
|
0x09,
|
||||||
|
0x05, # Usage (Gamepad),
|
||||||
|
0xA1,
|
||||||
|
0x01, # Collection (Application),
|
||||||
|
)
|
||||||
|
+ ((0x85, 0x04) ) #report id
|
||||||
|
+ (
|
||||||
|
0x15,
|
||||||
|
0x00, # Logical Minimum (0),
|
||||||
|
0x25,
|
||||||
|
0x01, # Logical Maximum (1),
|
||||||
|
0x35,
|
||||||
|
0x00, # Physical Minimum (0),
|
||||||
|
0x45,
|
||||||
|
0x01, # Physical Maximum (1),
|
||||||
|
0x75,
|
||||||
|
0x01, # Report Size (1),
|
||||||
|
0x95,
|
||||||
|
0x08, # Report Count (8),
|
||||||
|
0x05,
|
||||||
|
0x09, # Usage Page (Button),
|
||||||
|
0x19,
|
||||||
|
0x01, # Usage Minimum (01h),
|
||||||
|
0x29,
|
||||||
|
0x08, # Usage Maximum (08h),
|
||||||
|
0x81,
|
||||||
|
0x02, # Input (Variable),
|
||||||
|
0x05,
|
||||||
|
0x01, # Usage Page (Desktop),
|
||||||
|
0x26,
|
||||||
|
0xFF,
|
||||||
|
0x00, # Logical Maximum (255),
|
||||||
|
0x46,
|
||||||
|
0xFF,
|
||||||
|
0x00, # Physical Maximum (255),
|
||||||
|
0x09,
|
||||||
|
0x30, # Usage (X),
|
||||||
|
0x09,
|
||||||
|
0x31, # Usage (Y),
|
||||||
|
0x75,
|
||||||
|
0x08, # Report Size (8),
|
||||||
|
0x95,
|
||||||
|
0x02, # Report Count (2),
|
||||||
|
0x81,
|
||||||
|
0x02, # Input (Variable),
|
||||||
|
0xC0, # End Collection
|
||||||
|
))
|
||||||
|
# pylint: disable=missing-kwoa
|
||||||
|
my_gamepad = usb_hid.Device(
|
||||||
|
report_descriptor=xac_descriptor,
|
||||||
|
usage_page=1,
|
||||||
|
usage=5,
|
||||||
|
report_ids=(4,),
|
||||||
|
in_report_lengths=(3,),
|
||||||
|
out_report_lengths=(0,),)
|
||||||
|
print("Enabling XAC Gamepad")
|
||||||
|
usb_hid.enable((my_gamepad,))
|
||||||
90
TRRS_Trinkey_Demos/XAC_Joystick/code.py
Normal file
90
TRRS_Trinkey_Demos/XAC_Joystick/code.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 by John Park for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# adapted from Bill Binko's Chording Switches code
|
||||||
|
'''
|
||||||
|
Xbox Adaptive Controller USB port joystick
|
||||||
|
Use a two axis joystick, or combo of pots, soft pots, etc.
|
||||||
|
wired to TRRS 3.mm plug:
|
||||||
|
Tip = X
|
||||||
|
Ring 1 = Y
|
||||||
|
Ring 2 = GND
|
||||||
|
Sleeve = VCC
|
||||||
|
'''
|
||||||
|
import time
|
||||||
|
import array
|
||||||
|
import board
|
||||||
|
import analogio
|
||||||
|
import digitalio
|
||||||
|
#Custom version of Gamepad compatible w/the XBox Adaptive Controller (XAC)
|
||||||
|
import xac_gamepad
|
||||||
|
# pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
from XACsettings import *
|
||||||
|
|
||||||
|
time.sleep(1.0)
|
||||||
|
gp = xac_gamepad.XACGamepad()
|
||||||
|
|
||||||
|
class RollingAverage:
|
||||||
|
def __init__(self, size):
|
||||||
|
self.size=size
|
||||||
|
# pylint: disable=c-extension-no-member
|
||||||
|
self.buffer = array.array('d')
|
||||||
|
for _ in range(size):
|
||||||
|
self.buffer.append(0.0)
|
||||||
|
self.pos = 0
|
||||||
|
def addValue(self,val):
|
||||||
|
self.buffer[self.pos] = val
|
||||||
|
self.pos = (self.pos + 1) % self.size
|
||||||
|
def average(self):
|
||||||
|
return sum(self.buffer) / self.size
|
||||||
|
|
||||||
|
# Two analog inputs for TIP and RING_1
|
||||||
|
hor = analogio.AnalogIn(board.TIP)
|
||||||
|
vert = analogio.AnalogIn(board.RING_1)
|
||||||
|
|
||||||
|
# RING_2 as ground
|
||||||
|
ground = digitalio.DigitalInOut(board.RING_2)
|
||||||
|
ground.direction=digitalio.Direction.OUTPUT
|
||||||
|
ground.value = False
|
||||||
|
|
||||||
|
# SLEEVE as VCC (3.3V)
|
||||||
|
vcc = digitalio.DigitalInOut(board.SLEEVE)
|
||||||
|
vcc.direction=digitalio.Direction.OUTPUT
|
||||||
|
vcc.value = True
|
||||||
|
|
||||||
|
def range_map(value, in_min, in_max, out_min, out_max):
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
return int(max(out_min,min(out_max,(value - in_min) * (out_max - out_min) // (in_max - in_min) + out_min)))
|
||||||
|
|
||||||
|
# These two are how much we should smooth the joystick - higher numbers smooth more but add lag
|
||||||
|
VERT_AVG_COUNT=3
|
||||||
|
HOR_AVG_COUNT=3
|
||||||
|
#We need two Rolling Average Objects to smooth our values
|
||||||
|
xAvg = RollingAverage(HOR_AVG_COUNT)
|
||||||
|
yAvg = RollingAverage(VERT_AVG_COUNT)
|
||||||
|
|
||||||
|
gp.reset_all()
|
||||||
|
|
||||||
|
|
||||||
|
while True:
|
||||||
|
x = range_map(hor.value, 540, 65000, 0, 255)
|
||||||
|
y = range_map(vert.value, 65000, 540, 0, 255)
|
||||||
|
|
||||||
|
#Calculate the rolling average for the X and Y
|
||||||
|
lastXAvg = xAvg.average()
|
||||||
|
lastYAvg = yAvg.average()
|
||||||
|
|
||||||
|
#We know x and y, so do some smoothing
|
||||||
|
xAvg.addValue(x)
|
||||||
|
yAvg.addValue(y)
|
||||||
|
#We need to send integers so calculate the average and truncate it
|
||||||
|
newX = int(xAvg.average())
|
||||||
|
newY = int(yAvg.average())
|
||||||
|
|
||||||
|
#We only call move_joysticks if one of the values has changed from last time
|
||||||
|
if (newX != lastXAvg or newY != lastYAvg):
|
||||||
|
gp.move_joysticks(x=newX,y=newY)
|
||||||
|
# print(hor.value, vert.value) # print debug raw values
|
||||||
|
# print((newX, newY,)) # print debug remapped, averaged values
|
||||||
|
#Sleep to avoid overwhelming the XAC
|
||||||
|
time.sleep(0.05)
|
||||||
168
TRRS_Trinkey_Demos/XAC_Joystick/xac_gamepad.py
Normal file
168
TRRS_Trinkey_Demos/XAC_Joystick/xac_gamepad.py
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Bill Binko
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Dan Halbert for Adafruit Industries
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_hid.gamepad.Gamepad`
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
* Author(s): Dan Halbert
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
if sys.implementation.version[0] < 3:
|
||||||
|
raise ImportError('{0} is not supported in CircuitPython 2.x or lower'.format(__name__))
|
||||||
|
|
||||||
|
# pylint: disable=wrong-import-position
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import usb_hid
|
||||||
|
|
||||||
|
class XACGamepad:
|
||||||
|
"""Emulate a generic gamepad controller with 8 buttons,
|
||||||
|
numbered 1-8 and one joysticks, controlling
|
||||||
|
``x` and ``y`` values
|
||||||
|
|
||||||
|
The joystick values could be interpreted
|
||||||
|
differently by the receiving program: those are just the names used here.
|
||||||
|
The joystick values are in the range 0 to 255.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Create a Gamepad object that will send USB gamepad HID reports."""
|
||||||
|
self._hid_gamepad = None
|
||||||
|
for device in usb_hid.devices:
|
||||||
|
print(device)
|
||||||
|
if device.usage_page == 0x1 and device.usage == 0x05:
|
||||||
|
self._hid_gamepad = device
|
||||||
|
break
|
||||||
|
if not self._hid_gamepad:
|
||||||
|
raise OSError("Could not find an HID gamepad device.")
|
||||||
|
|
||||||
|
# Reuse this bytearray to send mouse reports.
|
||||||
|
# Typically controllers start numbering buttons at 1 rather than 0.
|
||||||
|
# report[0] buttons 1-8 (LSB is button 1)
|
||||||
|
# report[1] joystick 0 x: 0 to 255
|
||||||
|
# report[2] joystick 0 y: 0 to 255
|
||||||
|
self._report = bytearray(3)
|
||||||
|
|
||||||
|
# Remember the last report as well, so we can avoid sending
|
||||||
|
# duplicate reports.
|
||||||
|
self._last_report = bytearray(3)
|
||||||
|
|
||||||
|
# Store settings separately before putting into report. Saves code
|
||||||
|
# especially for buttons.
|
||||||
|
self._buttons_state = 0
|
||||||
|
self._joy_x = 0
|
||||||
|
self._joy_y = 0
|
||||||
|
|
||||||
|
# Send an initial report to test if HID device is ready.
|
||||||
|
# If not, wait a bit and try once more.
|
||||||
|
try:
|
||||||
|
self.reset_all()
|
||||||
|
except OSError:
|
||||||
|
time.sleep(1)
|
||||||
|
self.reset_all()
|
||||||
|
|
||||||
|
def press_buttons(self, *buttons):
|
||||||
|
"""Press and hold the given buttons. """
|
||||||
|
for button in buttons:
|
||||||
|
self._buttons_state |= 1 << self._validate_button_number(button) - 1
|
||||||
|
self._send()
|
||||||
|
|
||||||
|
def release_buttons(self, *buttons):
|
||||||
|
"""Release the given buttons. """
|
||||||
|
for button in buttons:
|
||||||
|
self._buttons_state &= ~(1 << self._validate_button_number(button) - 1)
|
||||||
|
self._send()
|
||||||
|
|
||||||
|
def release_all_buttons(self):
|
||||||
|
"""Release all the buttons."""
|
||||||
|
|
||||||
|
self._buttons_state = 0
|
||||||
|
self._send()
|
||||||
|
|
||||||
|
def click_buttons(self, *buttons):
|
||||||
|
"""Press and release the given buttons."""
|
||||||
|
self.press_buttons(*buttons)
|
||||||
|
self.release_buttons(*buttons)
|
||||||
|
|
||||||
|
def move_joysticks(self, x=None, y=None):
|
||||||
|
"""Set and send the given joystick values.
|
||||||
|
The joysticks will remain set with the given values until changed
|
||||||
|
|
||||||
|
One joystick provides ``x`` and ``y`` values,
|
||||||
|
and the other provides ``z`` and ``r_z`` (z rotation).
|
||||||
|
Any values left as ``None`` will not be changed.
|
||||||
|
|
||||||
|
All values must be in the range 0 to 255 inclusive.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
# Change x and y values only.
|
||||||
|
gp.move_joysticks(x=100, y=-50)
|
||||||
|
|
||||||
|
# Reset all joystick values to center position.
|
||||||
|
gp.move_joysticks(0, 0, 0, 0)
|
||||||
|
"""
|
||||||
|
if x is not None:
|
||||||
|
self._joy_x = self._validate_joystick_value(x)
|
||||||
|
if y is not None:
|
||||||
|
self._joy_y = self._validate_joystick_value(y)
|
||||||
|
self._send()
|
||||||
|
|
||||||
|
def reset_all(self):
|
||||||
|
"""Release all buttons and set joysticks to zero."""
|
||||||
|
self._buttons_state = 0
|
||||||
|
self._joy_x = 128
|
||||||
|
self._joy_y = 128
|
||||||
|
self._send(always=True)
|
||||||
|
|
||||||
|
def _send(self, always=False):
|
||||||
|
"""Send a report with all the existing settings.
|
||||||
|
If ``always`` is ``False`` (the default), send only if there have been changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
struct.pack_into('<BBB', self._report, 0,
|
||||||
|
self._buttons_state,
|
||||||
|
self._joy_x, self._joy_y)
|
||||||
|
|
||||||
|
if always or self._last_report != self._report:
|
||||||
|
self._hid_gamepad.send_report(self._report)
|
||||||
|
|
||||||
|
# Remember what we sent, without allocating new storage.
|
||||||
|
self._last_report[:] = self._report
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_button_number(button):
|
||||||
|
if not 1 <= button <= 8:
|
||||||
|
raise ValueError("Button number must in range 1 to 8")
|
||||||
|
return button
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_joystick_value(value):
|
||||||
|
if not 0 <= value <= 255:
|
||||||
|
raise ValueError("Joystick value must be in range 0 to 255")
|
||||||
|
return value
|
||||||
Loading…
Reference in a new issue