diff --git a/TRRS_Trinkey_Demos/XAC_Joystick/XACsettings.py b/TRRS_Trinkey_Demos/XAC_Joystick/XACsettings.py new file mode 100644 index 000000000..1a4a06cb8 --- /dev/null +++ b/TRRS_Trinkey_Demos/XAC_Joystick/XACsettings.py @@ -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 diff --git a/TRRS_Trinkey_Demos/XAC_Joystick/boot.py b/TRRS_Trinkey_Demos/XAC_Joystick/boot.py new file mode 100644 index 000000000..a8bf8b24f --- /dev/null +++ b/TRRS_Trinkey_Demos/XAC_Joystick/boot.py @@ -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,)) diff --git a/TRRS_Trinkey_Demos/XAC_Joystick/code.py b/TRRS_Trinkey_Demos/XAC_Joystick/code.py new file mode 100644 index 000000000..d368741ce --- /dev/null +++ b/TRRS_Trinkey_Demos/XAC_Joystick/code.py @@ -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) diff --git a/TRRS_Trinkey_Demos/XAC_Joystick/xac_gamepad.py b/TRRS_Trinkey_Demos/XAC_Joystick/xac_gamepad.py new file mode 100644 index 000000000..3d1cb97d9 --- /dev/null +++ b/TRRS_Trinkey_Demos/XAC_Joystick/xac_gamepad.py @@ -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('