Skip to content

Commit 90e4ca9

Browse files
authored
Merge pull request #19 from dhalbert/joystick
Gamepad support, with examples.
2 parents d57218e + 5669bb9 commit 90e4ca9

File tree

5 files changed

+296
-4
lines changed

5 files changed

+296
-4
lines changed

README.rst

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,26 @@ remote controls, or the multimedia keys on certain keyboards.
131131
cc = ConsumerControl()
132132
133133
# Raise volume.
134-
cc.send(ConsumerCode.VOLUME_INCREMENT)
134+
cc.send(ConsumerControlCode.VOLUME_INCREMENT)
135135
136136
# Pause or resume playback.
137-
cc.send(ConsumerCode.PLAY_PAUSE)
137+
cc.send(ConsumerControlCode.PLAY_PAUSE)
138+
139+
The ``Gamepad`` class emulates a two-joystick gamepad with 16 buttons.
140+
141+
*New in CircuitPython 3.0.*
142+
143+
.. code-block:: python
144+
145+
from adafruit_hid.gamepad import Gamepad
146+
147+
gp = Gamepad()
148+
149+
# Click gamepad buttons.
150+
gp.click_buttons(1, 7)
151+
152+
# Move joysticks.
153+
gp.move_joysticks(x=2, y=0, z=-20)
138154
139155
Contributing
140156
============

adafruit_hid/consumer_control.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ def send(self, consumer_code):
7272
from adafruit_hid.consumer_control_code import ConsumerControlCode
7373
7474
# Raise volume.
75-
consumer_control.send(ConsumerCode.VOLUME_INCREMENT)
75+
consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT)
7676
7777
# Advance to next track (song).
78-
consumer_control.send(ConsumerCode.SCAN_NEXT_TRACK)
78+
consumer_control.send(ConsumerControlCode.SCAN_NEXT_TRACK)
7979
"""
8080
self.usage_id[0] = consumer_code
8181
self.hid_consumer.send_report(self.report)

adafruit_hid/gamepad.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2018 Dan Halbert for Adafruit Industries
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
#
23+
24+
"""
25+
`adafruit_hid.gamepad.Gamepad`
26+
====================================================
27+
28+
* Author(s): Dan Halbert
29+
"""
30+
31+
import struct
32+
import time
33+
import usb_hid
34+
35+
class Gamepad:
36+
"""Emulate a generic gamepad controller with 16 buttons,
37+
numbered 1-16, and two joysticks, one controlling
38+
``x` and ``y`` values, and the other controlling ``z`` and
39+
``r_z`` (z rotation or ``Rz``) values.
40+
41+
The joystick values could be interpreted
42+
differently by the receiving program: those are just the names used here.
43+
The joystick values are in the range -127 to 127.
44+
"""
45+
46+
def __init__(self):
47+
"""Create a Gamepad object that will send USB gamepad HID reports."""
48+
self._hid_gamepad = None
49+
for device in usb_hid.devices:
50+
if device.usage_page == 0x1 and device.usage == 0x05:
51+
self._hid_gamepad = device
52+
break
53+
if not self._hid_gamepad:
54+
raise IOError("Could not find an HID gampead device.")
55+
56+
# Reuse this bytearray to send mouse reports.
57+
# Typically controllers start numbering buttons at 1 rather than 0.
58+
# report[0] buttons 1-8 (LSB is button 1)
59+
# report[1] buttons 9-16
60+
# report[2] joystick 0 x: -127 to 127
61+
# report[3] joystick 0 y: -127 to 127
62+
# report[4] joystick 1 x: -127 to 127
63+
# report[5] joystick 1 y: -127 to 127
64+
self._report = bytearray(6)
65+
66+
# Remember the last report as well, so we can avoid sending
67+
# duplicate reports.
68+
self._last_report = bytearray(6)
69+
70+
# Store settings separately before putting into report. Saves code
71+
# especially for buttons.
72+
self._buttons_state = 0
73+
self._joy_x = 0
74+
self._joy_y = 0
75+
self._joy_z = 0
76+
self._joy_r_z = 0
77+
78+
# Send an initial report to test if HID device is ready.
79+
# If not, wait a bit and try once more.
80+
try:
81+
self.reset_all()
82+
except OSError:
83+
time.sleep(1)
84+
self.reset_all()
85+
86+
def press_buttons(self, *buttons):
87+
"""Press and hold the given buttons. """
88+
for button in buttons:
89+
self._buttons_state |= 1 << self._validate_button_number(button) - 1
90+
self._send()
91+
92+
def release_buttons(self, *buttons):
93+
"""Release the given buttons. """
94+
for button in buttons:
95+
self._buttons_state &= ~(1 << self._validate_button_number(button) - 1)
96+
self._send()
97+
98+
def release_all_buttons(self):
99+
"""Release all the buttons."""
100+
101+
self._buttons_state = 0
102+
self._send()
103+
104+
def click_buttons(self, *buttons):
105+
"""Press and release the given buttons."""
106+
self.press_buttons(*buttons)
107+
self.release_buttons(*buttons)
108+
109+
def move_joysticks(self, x=None, y=None, z=None, r_z=None):
110+
"""Set and send the given joystick values.
111+
The joysticks will remain set with the given values until changed
112+
113+
One joystick provides ``x`` and ``y`` values,
114+
and the other provides ``z`` and ``r_z`` (z rotation).
115+
Any values left as ``None`` will not be changed.
116+
117+
All values must be in the range -127 to 127 inclusive.
118+
119+
Examples::
120+
121+
# Change x and y values only.
122+
gp.move_joysticks(x=100, y=-50)
123+
124+
# Reset all joystick values to center position.
125+
gp.move_joysticks(0, 0, 0, 0)
126+
"""
127+
if x is not None:
128+
self._joy_x = self._validate_joystick_value(x)
129+
if y is not None:
130+
self._joy_y = self._validate_joystick_value(y)
131+
if z is not None:
132+
self._joy_z = self._validate_joystick_value(z)
133+
if r_z is not None:
134+
self._joy_r_z = self._validate_joystick_value(r_z)
135+
self._send()
136+
137+
def reset_all(self):
138+
"""Release all buttons and set joysticks to zero."""
139+
self._buttons_state = 0
140+
self._joy_x = 0
141+
self._joy_y = 0
142+
self._joy_z = 0
143+
self._joy_r_z = 0
144+
self._send(always=True)
145+
146+
def _send(self, always=False):
147+
"""Send a report with all the existing settings.
148+
If ``always`` is ``False`` (the default), send only if there have been changes.
149+
"""
150+
struct.pack_into('<HBBBB', self._report, 0,
151+
self._buttons_state,
152+
self._joy_x, self._joy_y,
153+
self._joy_z, self._joy_r_z)
154+
155+
if always or self._last_report != self._report:
156+
self._hid_gamepad.send_report(self._report)
157+
# Remember what we sent, without allocating new storage.
158+
self._last_report[:] = self._report
159+
160+
@staticmethod
161+
def _validate_button_number(button):
162+
if not 1 <= button <= 16:
163+
raise ValueError("Button number must in range 1 to 16")
164+
return button
165+
166+
@staticmethod
167+
def _validate_joystick_value(value):
168+
if not -127 <= value <= 127:
169+
raise ValueError("Joystick value must be in range -127 to 127")
170+
return value

examples/joywing_gamepad.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Use Joy FeatherWing to drive Gamepad.
2+
3+
import time
4+
5+
import board
6+
import busio
7+
import adafruit_seesaw
8+
from adafruit_hid.gamepad import Gamepad
9+
from micropython import const
10+
11+
def range_map(value, in_min, in_max, out_min, out_max):
12+
return (value - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
13+
14+
BUTTON_RIGHT = const(6)
15+
BUTTON_DOWN = const(7)
16+
BUTTON_LEFT = const(9)
17+
BUTTON_UP = const(10)
18+
BUTTON_SEL = const(14)
19+
button_mask = const((1 << BUTTON_RIGHT) |
20+
(1 << BUTTON_DOWN) |
21+
(1 << BUTTON_LEFT) |
22+
(1 << BUTTON_UP) |
23+
(1 << BUTTON_SEL))
24+
25+
i2c = busio.I2C(board.SCL, board.SDA)
26+
27+
ss = adafruit_seesaw.Seesaw(i2c)
28+
29+
ss.pin_mode_bulk(button_mask, ss.INPUT_PULLUP)
30+
31+
last_game_x = 0
32+
last_game_y = 0
33+
34+
g = Gamepad()
35+
36+
while True:
37+
x = ss.analog_read(2)
38+
y = ss.analog_read(3)
39+
40+
game_x = range_map(x, 0, 1023, -127, 127)
41+
game_y = range_map(y, 0, 1023, -127, 127)
42+
if last_game_x != game_x or last_game_y != game_y:
43+
last_game_x = game_x
44+
last_game_y = game_y
45+
print(game_x, game_y)
46+
g.move_joysticks(x=game_x, y=game_y)
47+
48+
buttons = (BUTTON_RIGHT, BUTTON_DOWN, BUTTON_LEFT, BUTTON_UP, BUTTON_SEL)
49+
button_state = [False] * len(buttons)
50+
for i, button in enumerate(buttons):
51+
buttons = ss.digital_read_bulk(button_mask)
52+
if not (buttons & (1 << button) and not button_state[i]):
53+
g.press_buttons(i+1)
54+
print("Press", i+1)
55+
button_state[i] = True
56+
elif button_state[i]:
57+
g.release_buttons(i+1)
58+
print("Release", i+1)
59+
button_state[i] = False
60+
61+
time.sleep(.01)

examples/simple_gamepad.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import analogio
2+
import board
3+
import digitalio
4+
5+
from adafruit_hid.gamepad import Gamepad
6+
7+
gp = Gamepad()
8+
9+
# Create some buttons. The physical buttons are connected
10+
# to ground on one side and these and these pins on the other.
11+
button_pins = (board.D2, board.D3, board.D4, board.D5)
12+
13+
# Map the buttons to button numbers on the Gamepad.
14+
# gamepad_buttons[i] will send that button number when buttons[i]
15+
# is pushed.
16+
gamepad_buttons = (1, 2, 8, 15)
17+
18+
buttons = [digitalio.DigitalInOut(pin) for pin in button_pins]
19+
for button in buttons:
20+
button.direction = digitalio.Direction.INPUT
21+
button.pull = digitalio.Pull.UP
22+
23+
# Connect an analog two-axis joystick to A4 and A5.
24+
ax = analogio.AnalogIn(board.A4)
25+
ay = analogio.AnalogIn(board.A5)
26+
27+
# Equivalent of Arduino's map() function.
28+
def range_map(x, in_min, in_max, out_min, out_max):
29+
return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
30+
31+
while True:
32+
# Buttons are grounded when pressed (.value = False).
33+
for i, button in enumerate(buttons):
34+
gamepad_button_num = gamepad_buttons[i]
35+
if button.value:
36+
gp.release_buttons(gamepad_button_num)
37+
print(" release", gamepad_button_num, end='')
38+
else:
39+
gp.press_buttons(gamepad_button_num)
40+
print(" press", gamepad_button_num, end='')
41+
42+
# Convert range[0, 65535] to -127 to 127
43+
gp.move_joysticks(x=range_map(ax.value, 0, 65535, -127, 127),
44+
y=range_map(ay.value, 0, 65535, -127, 127))
45+
print(" x", ax.value, "y", ay.value)

0 commit comments

Comments
 (0)