Skip to content

Commit

Permalink
Merge pull request #19 from dhalbert/joystick
Browse files Browse the repository at this point in the history
Gamepad support, with examples.
  • Loading branch information
tannewt authored Apr 24, 2018
2 parents d57218e + 5669bb9 commit 90e4ca9
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 4 deletions.
20 changes: 18 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,26 @@ remote controls, or the multimedia keys on certain keyboards.
cc = ConsumerControl()
# Raise volume.
cc.send(ConsumerCode.VOLUME_INCREMENT)
cc.send(ConsumerControlCode.VOLUME_INCREMENT)
# Pause or resume playback.
cc.send(ConsumerCode.PLAY_PAUSE)
cc.send(ConsumerControlCode.PLAY_PAUSE)
The ``Gamepad`` class emulates a two-joystick gamepad with 16 buttons.

*New in CircuitPython 3.0.*

.. code-block:: python
from adafruit_hid.gamepad import Gamepad
gp = Gamepad()
# Click gamepad buttons.
gp.click_buttons(1, 7)
# Move joysticks.
gp.move_joysticks(x=2, y=0, z=-20)
Contributing
============
Expand Down
4 changes: 2 additions & 2 deletions adafruit_hid/consumer_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ def send(self, consumer_code):
from adafruit_hid.consumer_control_code import ConsumerControlCode
# Raise volume.
consumer_control.send(ConsumerCode.VOLUME_INCREMENT)
consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT)
# Advance to next track (song).
consumer_control.send(ConsumerCode.SCAN_NEXT_TRACK)
consumer_control.send(ConsumerControlCode.SCAN_NEXT_TRACK)
"""
self.usage_id[0] = consumer_code
self.hid_consumer.send_report(self.report)
Expand Down
170 changes: 170 additions & 0 deletions adafruit_hid/gamepad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# 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 struct
import time
import usb_hid

class Gamepad:
"""Emulate a generic gamepad controller with 16 buttons,
numbered 1-16, and two joysticks, one controlling
``x` and ``y`` values, and the other controlling ``z`` and
``r_z`` (z rotation or ``Rz``) 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 -127 to 127.
"""

def __init__(self):
"""Create a Gamepad object that will send USB gamepad HID reports."""
self._hid_gamepad = None
for device in usb_hid.devices:
if device.usage_page == 0x1 and device.usage == 0x05:
self._hid_gamepad = device
break
if not self._hid_gamepad:
raise IOError("Could not find an HID gampead 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] buttons 9-16
# report[2] joystick 0 x: -127 to 127
# report[3] joystick 0 y: -127 to 127
# report[4] joystick 1 x: -127 to 127
# report[5] joystick 1 y: -127 to 127
self._report = bytearray(6)

# Remember the last report as well, so we can avoid sending
# duplicate reports.
self._last_report = bytearray(6)

# Store settings separately before putting into report. Saves code
# especially for buttons.
self._buttons_state = 0
self._joy_x = 0
self._joy_y = 0
self._joy_z = 0
self._joy_r_z = 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, z=None, r_z=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 -127 to 127 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)
if z is not None:
self._joy_z = self._validate_joystick_value(z)
if r_z is not None:
self._joy_r_z = self._validate_joystick_value(r_z)
self._send()

def reset_all(self):
"""Release all buttons and set joysticks to zero."""
self._buttons_state = 0
self._joy_x = 0
self._joy_y = 0
self._joy_z = 0
self._joy_r_z = 0
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('<HBBBB', self._report, 0,
self._buttons_state,
self._joy_x, self._joy_y,
self._joy_z, self._joy_r_z)

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 <= 16:
raise ValueError("Button number must in range 1 to 16")
return button

@staticmethod
def _validate_joystick_value(value):
if not -127 <= value <= 127:
raise ValueError("Joystick value must be in range -127 to 127")
return value
61 changes: 61 additions & 0 deletions examples/joywing_gamepad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Use Joy FeatherWing to drive Gamepad.

import time

import board
import busio
import adafruit_seesaw
from adafruit_hid.gamepad import Gamepad
from micropython import const

def range_map(value, in_min, in_max, out_min, out_max):
return (value - in_min) * (out_max - out_min) // (in_max - in_min) + out_min

BUTTON_RIGHT = const(6)
BUTTON_DOWN = const(7)
BUTTON_LEFT = const(9)
BUTTON_UP = const(10)
BUTTON_SEL = const(14)
button_mask = const((1 << BUTTON_RIGHT) |
(1 << BUTTON_DOWN) |
(1 << BUTTON_LEFT) |
(1 << BUTTON_UP) |
(1 << BUTTON_SEL))

i2c = busio.I2C(board.SCL, board.SDA)

ss = adafruit_seesaw.Seesaw(i2c)

ss.pin_mode_bulk(button_mask, ss.INPUT_PULLUP)

last_game_x = 0
last_game_y = 0

g = Gamepad()

while True:
x = ss.analog_read(2)
y = ss.analog_read(3)

game_x = range_map(x, 0, 1023, -127, 127)
game_y = range_map(y, 0, 1023, -127, 127)
if last_game_x != game_x or last_game_y != game_y:
last_game_x = game_x
last_game_y = game_y
print(game_x, game_y)
g.move_joysticks(x=game_x, y=game_y)

buttons = (BUTTON_RIGHT, BUTTON_DOWN, BUTTON_LEFT, BUTTON_UP, BUTTON_SEL)
button_state = [False] * len(buttons)
for i, button in enumerate(buttons):
buttons = ss.digital_read_bulk(button_mask)
if not (buttons & (1 << button) and not button_state[i]):
g.press_buttons(i+1)
print("Press", i+1)
button_state[i] = True
elif button_state[i]:
g.release_buttons(i+1)
print("Release", i+1)
button_state[i] = False

time.sleep(.01)
45 changes: 45 additions & 0 deletions examples/simple_gamepad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import analogio
import board
import digitalio

from adafruit_hid.gamepad import Gamepad

gp = Gamepad()

# Create some buttons. The physical buttons are connected
# to ground on one side and these and these pins on the other.
button_pins = (board.D2, board.D3, board.D4, board.D5)

# Map the buttons to button numbers on the Gamepad.
# gamepad_buttons[i] will send that button number when buttons[i]
# is pushed.
gamepad_buttons = (1, 2, 8, 15)

buttons = [digitalio.DigitalInOut(pin) for pin in button_pins]
for button in buttons:
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP

# Connect an analog two-axis joystick to A4 and A5.
ax = analogio.AnalogIn(board.A4)
ay = analogio.AnalogIn(board.A5)

# Equivalent of Arduino's map() function.
def range_map(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min

while True:
# Buttons are grounded when pressed (.value = False).
for i, button in enumerate(buttons):
gamepad_button_num = gamepad_buttons[i]
if button.value:
gp.release_buttons(gamepad_button_num)
print(" release", gamepad_button_num, end='')
else:
gp.press_buttons(gamepad_button_num)
print(" press", gamepad_button_num, end='')

# Convert range[0, 65535] to -127 to 127
gp.move_joysticks(x=range_map(ax.value, 0, 65535, -127, 127),
y=range_map(ay.value, 0, 65535, -127, 127))
print(" x", ax.value, "y", ay.value)

0 comments on commit 90e4ca9

Please sign in to comment.