Skip to content

Commit ad042d2

Browse files
authored
Merge pull request #2832 from jedgarpark/xac-joystick
XAC joysticks first commit
2 parents b4d8dd5 + 16e4fc0 commit ad042d2

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# SPDX-FileCopyrightText: 2024 Bill Binko
2+
# SPDX-License-Identifier: MIT
3+
4+
#Change this to True to swap horizonatal and vertical axes
5+
swapAxes = False
6+
7+
#Change this to True to invert (flip) the horizontal axis
8+
invertHor = False
9+
10+
#Change this to True to invert (flip) the vertical axis
11+
invertVert = True
12+
13+
#Increase this to make the motion smoother (with more lag)
14+
#Decrease to make more responsive (Min=1 Default=3 Max=Any but>20 is unreasonable)
15+
smoothingFactor = 2
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-FileCopyrightText: 2024 Bill Binko
2+
# SPDX-License-Identifier: MIT
3+
4+
import usb_midi
5+
import usb_hid
6+
7+
print("In boot.py")
8+
9+
# storage.disable_usb_device()
10+
11+
# usb_cdc.enable(console=True, data=True)
12+
13+
usb_midi.disable()
14+
xac_descriptor=bytes(
15+
# This descriptor mimics the simple joystick from PDP that the XBox likes
16+
(
17+
0x05,
18+
0x01, # Usage Page (Desktop),
19+
0x09,
20+
0x05, # Usage (Gamepad),
21+
0xA1,
22+
0x01, # Collection (Application),
23+
)
24+
+ ((0x85, 0x04) ) #report id
25+
+ (
26+
0x15,
27+
0x00, # Logical Minimum (0),
28+
0x25,
29+
0x01, # Logical Maximum (1),
30+
0x35,
31+
0x00, # Physical Minimum (0),
32+
0x45,
33+
0x01, # Physical Maximum (1),
34+
0x75,
35+
0x01, # Report Size (1),
36+
0x95,
37+
0x08, # Report Count (8),
38+
0x05,
39+
0x09, # Usage Page (Button),
40+
0x19,
41+
0x01, # Usage Minimum (01h),
42+
0x29,
43+
0x08, # Usage Maximum (08h),
44+
0x81,
45+
0x02, # Input (Variable),
46+
0x05,
47+
0x01, # Usage Page (Desktop),
48+
0x26,
49+
0xFF,
50+
0x00, # Logical Maximum (255),
51+
0x46,
52+
0xFF,
53+
0x00, # Physical Maximum (255),
54+
0x09,
55+
0x30, # Usage (X),
56+
0x09,
57+
0x31, # Usage (Y),
58+
0x75,
59+
0x08, # Report Size (8),
60+
0x95,
61+
0x02, # Report Count (2),
62+
0x81,
63+
0x02, # Input (Variable),
64+
0xC0, # End Collection
65+
))
66+
# pylint: disable=missing-kwoa
67+
my_gamepad = usb_hid.Device(
68+
report_descriptor=xac_descriptor,
69+
usage_page=1,
70+
usage=5,
71+
report_ids=(4,),
72+
in_report_lengths=(3,),
73+
out_report_lengths=(0,),)
74+
print("Enabling XAC Gamepad")
75+
usb_hid.enable((my_gamepad,))
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-FileCopyrightText: 2024 by John Park for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
# adapted from Bill Binko's Chording Switches code
5+
'''
6+
Xbox Adaptive Controller USB port joystick
7+
Use a two axis joystick, or combo of pots, soft pots, etc.
8+
wired to TRRS 3.mm plug:
9+
Tip = X
10+
Ring 1 = Y
11+
Ring 2 = GND
12+
Sleeve = VCC
13+
'''
14+
import time
15+
import array
16+
import board
17+
import analogio
18+
import digitalio
19+
#Custom version of Gamepad compatible w/the XBox Adaptive Controller (XAC)
20+
import xac_gamepad
21+
# pylint: disable=wildcard-import, unused-wildcard-import
22+
from XACsettings import *
23+
24+
time.sleep(1.0)
25+
gp = xac_gamepad.XACGamepad()
26+
27+
class RollingAverage:
28+
def __init__(self, size):
29+
self.size=size
30+
# pylint: disable=c-extension-no-member
31+
self.buffer = array.array('d')
32+
for _ in range(size):
33+
self.buffer.append(0.0)
34+
self.pos = 0
35+
def addValue(self,val):
36+
self.buffer[self.pos] = val
37+
self.pos = (self.pos + 1) % self.size
38+
def average(self):
39+
return sum(self.buffer) / self.size
40+
41+
# Two analog inputs for TIP and RING_1
42+
hor = analogio.AnalogIn(board.TIP)
43+
vert = analogio.AnalogIn(board.RING_1)
44+
45+
# RING_2 as ground
46+
ground = digitalio.DigitalInOut(board.RING_2)
47+
ground.direction=digitalio.Direction.OUTPUT
48+
ground.value = False
49+
50+
# SLEEVE as VCC (3.3V)
51+
vcc = digitalio.DigitalInOut(board.SLEEVE)
52+
vcc.direction=digitalio.Direction.OUTPUT
53+
vcc.value = True
54+
55+
def range_map(value, in_min, in_max, out_min, out_max):
56+
# pylint: disable=line-too-long
57+
return int(max(out_min,min(out_max,(value - in_min) * (out_max - out_min) // (in_max - in_min) + out_min)))
58+
59+
# These two are how much we should smooth the joystick - higher numbers smooth more but add lag
60+
VERT_AVG_COUNT=3
61+
HOR_AVG_COUNT=3
62+
#We need two Rolling Average Objects to smooth our values
63+
xAvg = RollingAverage(HOR_AVG_COUNT)
64+
yAvg = RollingAverage(VERT_AVG_COUNT)
65+
66+
gp.reset_all()
67+
68+
69+
while True:
70+
x = range_map(hor.value, 540, 65000, 0, 255)
71+
y = range_map(vert.value, 65000, 540, 0, 255)
72+
73+
#Calculate the rolling average for the X and Y
74+
lastXAvg = xAvg.average()
75+
lastYAvg = yAvg.average()
76+
77+
#We know x and y, so do some smoothing
78+
xAvg.addValue(x)
79+
yAvg.addValue(y)
80+
#We need to send integers so calculate the average and truncate it
81+
newX = int(xAvg.average())
82+
newY = int(yAvg.average())
83+
84+
#We only call move_joysticks if one of the values has changed from last time
85+
if (newX != lastXAvg or newY != lastYAvg):
86+
gp.move_joysticks(x=newX,y=newY)
87+
# print(hor.value, vert.value) # print debug raw values
88+
# print((newX, newY,)) # print debug remapped, averaged values
89+
#Sleep to avoid overwhelming the XAC
90+
time.sleep(0.05)
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# SPDX-FileCopyrightText: 2024 Bill Binko
2+
# SPDX-License-Identifier: MIT
3+
4+
# The MIT License (MIT)
5+
#
6+
# Copyright (c) 2018 Dan Halbert for Adafruit Industries
7+
#
8+
# Permission is hereby granted, free of charge, to any person obtaining a copy
9+
# of this software and associated documentation files (the "Software"), to deal
10+
# in the Software without restriction, including without limitation the rights
11+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
# copies of the Software, and to permit persons to whom the Software is
13+
# furnished to do so, subject to the following conditions:
14+
#
15+
# The above copyright notice and this permission notice shall be included in
16+
# all copies or substantial portions of the Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
# THE SOFTWARE.
25+
#
26+
27+
"""
28+
`adafruit_hid.gamepad.Gamepad`
29+
====================================================
30+
31+
* Author(s): Dan Halbert
32+
"""
33+
34+
import sys
35+
if sys.implementation.version[0] < 3:
36+
raise ImportError('{0} is not supported in CircuitPython 2.x or lower'.format(__name__))
37+
38+
# pylint: disable=wrong-import-position
39+
import struct
40+
import time
41+
import usb_hid
42+
43+
class XACGamepad:
44+
"""Emulate a generic gamepad controller with 8 buttons,
45+
numbered 1-8 and one joysticks, controlling
46+
``x` and ``y`` values
47+
48+
The joystick values could be interpreted
49+
differently by the receiving program: those are just the names used here.
50+
The joystick values are in the range 0 to 255.
51+
"""
52+
53+
def __init__(self):
54+
"""Create a Gamepad object that will send USB gamepad HID reports."""
55+
self._hid_gamepad = None
56+
for device in usb_hid.devices:
57+
print(device)
58+
if device.usage_page == 0x1 and device.usage == 0x05:
59+
self._hid_gamepad = device
60+
break
61+
if not self._hid_gamepad:
62+
raise OSError("Could not find an HID gamepad device.")
63+
64+
# Reuse this bytearray to send mouse reports.
65+
# Typically controllers start numbering buttons at 1 rather than 0.
66+
# report[0] buttons 1-8 (LSB is button 1)
67+
# report[1] joystick 0 x: 0 to 255
68+
# report[2] joystick 0 y: 0 to 255
69+
self._report = bytearray(3)
70+
71+
# Remember the last report as well, so we can avoid sending
72+
# duplicate reports.
73+
self._last_report = bytearray(3)
74+
75+
# Store settings separately before putting into report. Saves code
76+
# especially for buttons.
77+
self._buttons_state = 0
78+
self._joy_x = 0
79+
self._joy_y = 0
80+
81+
# Send an initial report to test if HID device is ready.
82+
# If not, wait a bit and try once more.
83+
try:
84+
self.reset_all()
85+
except OSError:
86+
time.sleep(1)
87+
self.reset_all()
88+
89+
def press_buttons(self, *buttons):
90+
"""Press and hold the given buttons. """
91+
for button in buttons:
92+
self._buttons_state |= 1 << self._validate_button_number(button) - 1
93+
self._send()
94+
95+
def release_buttons(self, *buttons):
96+
"""Release the given buttons. """
97+
for button in buttons:
98+
self._buttons_state &= ~(1 << self._validate_button_number(button) - 1)
99+
self._send()
100+
101+
def release_all_buttons(self):
102+
"""Release all the buttons."""
103+
104+
self._buttons_state = 0
105+
self._send()
106+
107+
def click_buttons(self, *buttons):
108+
"""Press and release the given buttons."""
109+
self.press_buttons(*buttons)
110+
self.release_buttons(*buttons)
111+
112+
def move_joysticks(self, x=None, y=None):
113+
"""Set and send the given joystick values.
114+
The joysticks will remain set with the given values until changed
115+
116+
One joystick provides ``x`` and ``y`` values,
117+
and the other provides ``z`` and ``r_z`` (z rotation).
118+
Any values left as ``None`` will not be changed.
119+
120+
All values must be in the range 0 to 255 inclusive.
121+
122+
Examples::
123+
124+
# Change x and y values only.
125+
gp.move_joysticks(x=100, y=-50)
126+
127+
# Reset all joystick values to center position.
128+
gp.move_joysticks(0, 0, 0, 0)
129+
"""
130+
if x is not None:
131+
self._joy_x = self._validate_joystick_value(x)
132+
if y is not None:
133+
self._joy_y = self._validate_joystick_value(y)
134+
self._send()
135+
136+
def reset_all(self):
137+
"""Release all buttons and set joysticks to zero."""
138+
self._buttons_state = 0
139+
self._joy_x = 128
140+
self._joy_y = 128
141+
self._send(always=True)
142+
143+
def _send(self, always=False):
144+
"""Send a report with all the existing settings.
145+
If ``always`` is ``False`` (the default), send only if there have been changes.
146+
"""
147+
148+
struct.pack_into('<BBB', self._report, 0,
149+
self._buttons_state,
150+
self._joy_x, self._joy_y)
151+
152+
if always or self._last_report != self._report:
153+
self._hid_gamepad.send_report(self._report)
154+
155+
# Remember what we sent, without allocating new storage.
156+
self._last_report[:] = self._report
157+
158+
@staticmethod
159+
def _validate_button_number(button):
160+
if not 1 <= button <= 8:
161+
raise ValueError("Button number must in range 1 to 8")
162+
return button
163+
164+
@staticmethod
165+
def _validate_joystick_value(value):
166+
if not 0 <= value <= 255:
167+
raise ValueError("Joystick value must be in range 0 to 255")
168+
return value

0 commit comments

Comments
 (0)