XAC joysticks first commit

This commit is contained in:
John Park 2024-06-07 07:34:37 -07:00
parent b4d8dd5287
commit 16e4fc0255
4 changed files with 348 additions and 0 deletions

View 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

View 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,))

View 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)

View 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