Skip to content

Implementation sketch for 16 bit axes. #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion joystick_xl/hid.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

def create_joystick(
axes: int = 4,
axes16: int = 0,
buttons: int = 16,
hats: int = 1,
report_id: int = 0x04,
Expand Down Expand Up @@ -39,13 +40,20 @@ def create_joystick(

"""
_num_axes = axes
_num_axes16 = axes16
_num_buttons = buttons
_num_hats = hats

# Validate the number of configured axes, buttons and hats.
if _num_axes < 0 or _num_axes > 8:
raise ValueError("Axis count must be from 0-8.")


if _num_axes16 < 0 or _num_axes16 > 8:
raise ValueError("Axis 16 count must be from 0-8.")

if _num_axes + _num_axes16 > 8:
raise ValueError("Total axis count must be from 0-8.")

if _num_buttons < 0 or _num_buttons > 128:
raise ValueError("Button count must be from 0-128.")

Expand Down Expand Up @@ -81,6 +89,23 @@ def create_joystick(

_report_length = _num_axes


if _num_axes16:
for i in range(_num_axes16):
_descriptor.extend(bytes((
0x09, min(0x30 + i + _num_axes, 0x36) # : USAGE (X,Y,Z,Rx,Ry,Rz,S0,S1)
)))

_descriptor.extend(bytes((
0x15, 0x00, # : LOGICAL_MINIMUM (0)
0x26, 0xFF, 0xFF, # : LOGICAL_MAXIMUM (65535)
0x75, 0x10, # : REPORT_SIZE (16)
0x95, _num_axes16, # : REPORT_COUNT (num_axes)
0x81, 0x02, # : INPUT (Data,Var,Abs)
)))

_report_length += 2*_num_axes16

if _num_hats:
for i in range(_num_hats):
_descriptor.extend(bytes((
Expand Down Expand Up @@ -142,6 +167,8 @@ def create_joystick(
"with",
_num_axes,
"axes,",
_num_axes16,
"hd axes,",
_num_buttons,
"buttons and",
_num_hats,
Expand Down
227 changes: 227 additions & 0 deletions joystick_xl/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,233 @@ def _update(self) -> int:
return self._value



class Axis16:
"""Data source storage and scaling/deadband processing for an axis input."""

MIN = 0
"""Lowest possible axis value for USB HID reports."""

MAX = 65535
"""Highest possible axis value for USB HID reports."""

IDLE = 32768
"""Idle/Center axis value for USB HID reports."""

X = 0
"""Alias for the X-axis index."""

Y = 1
"""Alias for the Y-axis index."""

Z = 2
"""Alias for the Z-axis index."""

RX = 3
"""Alias for the RX-axis index."""

RY = 4
"""Alias for the RY-axis index."""

RZ = 5
"""Alias for the RZ-axis index."""

S0 = 6
"""Alias for the S0-axis index."""

S1 = 7
"""Alias for the S1-axis index."""

@property
def value(self) -> int:
"""
Get the current, fully processed value of this axis.

:return: ``0`` to ``65535``, ``32768`` if idle/centered or bypassed.
:rtype: int
"""
new_value = self._update()

if self.bypass:
return Axis.IDLE
else:
return new_value

@property
def source_value(self) -> int:
"""
Get the raw source input value.

*(For VirtualInput sources, this property can also be set.)*

:return: ``0`` to ``65535``
:rtype: int
"""
return self._source.value

@source_value.setter
def source_value(self, value: int) -> None:
"""Set the raw source value for a VirtualInput axis source."""
if isinstance(self._source, VirtualInput):
self._source.value = value
else:
raise TypeError("Only VirtualInput source values can be set manually.")

@property
def min(self) -> int:
"""
Get the configured minimum raw ``analogio`` input value.

:return: ``0`` to ``65535``
:rtype: int
"""
return self._min

@property
def max(self) -> int:
"""
Get the configured maximum raw ``analogio`` input value.

:return: ``0`` to ``65535``
:rtype: int
"""
return self._max

@property
def deadband(self) -> int:
"""
Get the raw, absolute value of the configured deadband.

:return: ``0`` to ``65535``
:rtype: int
"""
return self._deadband

@property
def invert(self) -> bool:
"""
Return ``True`` if the raw `analogio` input value is inverted.

:return: ``True`` if inverted, ``False`` otherwise
:rtype: bool
"""
return self._invert

def __init__(
self,
source=None,
deadband: int = 0,
min: int = 0,
max: int = 65535,
invert: bool = False,
bypass: bool = False,
) -> None:
"""
Provide data source storage and scaling/deadband processing for an axis input.

:param source: CircuitPython pin identifier (i.e. ``board.A0``) or any object
with an int ``.value`` attribute. (Defaults to ``None``, which will create
a ``VirtualInput`` source instead.)
:type source: Any, optional
:param deadband: Raw, absolute value of the deadband to apply around the
midpoint of the raw source value. The deadband is used to prevent an axis
from registering minimal values when it is centered. Setting the deadband
value to ``250`` means raw input values +/- 250 from the midpoint will all
be treated as the midpoint. (defaults to ``0``)
:type deadband: int, optional
:param min: The raw input value that corresponds to a scaled axis value of 0.
Any raw input value <= to this value will get scaled to 0. Useful if the
component used to generate the raw input never actually reaches 0.
(defaults to ``0``)
:type min: int, optional
:param max: The raw input value that corresponds to a scaled axis value of 255.
Any raw input value >= to this value will get scaled to 255. Useful if the
component used to generate the raw input never actually reaches 65535.
(defaults to ``65535``)
:type max: int, optional
:param invert: Set to ``True`` to invert the scaled axis value. Useful if the
physical orientation of the component used to generate the raw axis input
does not match the logical direction of the axis input.
(defaults to ``False``)
:type invert: bool, optional
:param bypass: Set to ``True`` to make the axis always appear ``centered``
in USB HID reports back to the host device. (Defaults to ``False``)
:type bypass: bool, optional
"""
self._source = Axis._initialize_source(source)
self._deadband = deadband
self._min = min
self._max = max
self._invert = invert
self._value = Axis.IDLE
self._last_source_value = Axis.IDLE

self.bypass = bypass
"""Set to ``True`` to make the axis always appear idle/centered."""

# calculate raw input midpoint and scaled deadband range
self._raw_midpoint = self._min + ((self._max - self._min) // 2)
self._db_range = self._max - self._min - (self._deadband * 2) + 1

self._update()

@staticmethod
def _initialize_source(source):
"""
Configure a source as an on-board pin, off-board input or VirtualInput.

:param source: CircuitPython pin identifier (i.e. ``board.A3``), any object
with an int ``.value`` attribute or a ``VirtualInput`` object.
:type source: Any
:return: A fully configured analog source pin or virtual input.
:rtype: AnalogIn or VirtualInput
"""
if source is None:
return VirtualInput(value=32768)
elif isinstance(source, Pin):
return AnalogIn(source)
elif hasattr(source, "value") and isinstance(source.value, int):
return source
else:
raise TypeError("Incompatible axis source specified.")

def _update(self) -> int:
"""
Read raw input data and convert it to a joystick-compatible value.

:return: ``0`` to ``65535``, ``32768`` if idle/centered.
:rtype: int
"""
source_value = self._source.value

# short-circuit processing if the source value hasn't changed
if source_value == self._last_source_value:
return self._value

self._last_source_value = source_value

# clamp raw input value to specified min/max
new_value = min(max(source_value, self._min), self._max)

# account for deadband
if new_value < (self._raw_midpoint - self._deadband):
new_value -= self._min
elif new_value > (self._raw_midpoint + self._deadband):
new_value = new_value - self._min - (self._deadband * 2)
else:
new_value = self._db_range // 2

# calculate scaled joystick-compatible value and clamp to 0-255
new_value = min(new_value * 65536 // self._db_range, 65535)

# invert the axis if necessary
if self._invert:
self._value = 65535 - new_value
else:
self._value = new_value

return self._value

class Button:
"""Data source storage and value processing for a button input."""

Expand Down
27 changes: 22 additions & 5 deletions joystick_xl/joystick.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
pass

from joystick_xl.hid import _get_device
from joystick_xl.inputs import Axis, Button, Hat
from joystick_xl.inputs import Axis, Axis16, Button, Hat


class Joystick:
Expand All @@ -24,6 +24,9 @@ class Joystick:
_num_axes = 0
"""The number of axes this joystick can support."""

_num_axes16 = 0
"""The number of hd axes this joystick can support."""

_num_buttons = 0
"""The number of buttons this joystick can support."""

Expand All @@ -38,6 +41,11 @@ def num_axes(self) -> int:
"""Return the number of available axes in the USB HID descriptor."""
return self._num_axes

@property
def num_axes16(self) -> int:
"""Return the number of available hd axes in the USB HID descriptor."""
return self._num_axes16

@property
def num_buttons(self) -> int:
"""Return the number of available buttons in the USB HID descriptor."""
Expand Down Expand Up @@ -71,9 +79,10 @@ def __init__(self) -> None:
if len(config) < 4:
raise (ValueError)
Joystick._num_axes = config[0]
Joystick._num_buttons = config[1]
Joystick._num_hats = config[2]
Joystick._report_size = config[3]
Joystick._num_axes16 = config[1]
Joystick._num_buttons = config[2]
Joystick._num_hats = config[3]
Joystick._report_size = config[4]
break
if Joystick._report_size == 0:
raise (ValueError)
Expand All @@ -92,6 +101,9 @@ def __init__(self) -> None:
for _ in range(self.num_axes):
self._axis_states.append(Axis.IDLE)
self._format += "B"
for _ in range(self.num_axes16):
self._axis_states.append(Axis.IDLE)
self._format += "H"

self.hat = list()
"""List of hat inputs associated with this joystick through ``add_input``."""
Expand Down Expand Up @@ -136,7 +148,7 @@ def _validate_axis_value(axis: int, value: int) -> bool:
if axis + 1 > Joystick._num_axes:
raise ValueError("Specified axis is out of range.")
if not Axis.MIN <= value <= Axis.MAX:
raise ValueError("Axis value must be in range 0 to 255")
raise ValueError("Axis value must be in range valid range")
return True

@staticmethod
Expand Down Expand Up @@ -203,6 +215,11 @@ def add_input(self, *input: Union[Axis, Button, Hat]) -> None:
self.axis.append(i)
else:
raise OverflowError("List is full, cannot add another axis.")
if isinstance(i, Axis16):
if len(self.axis) < self._num_axes + self._num_axes16:
self.axis.append(i)
else:
raise OverflowError("List is full, cannot add another axis 16.")
elif isinstance(i, Button):
if len(self.button) < self._num_buttons:
self.button.append(i)
Expand Down