[Перевод] Подключение геймпада к Raspberry Pi
Автор статьи, перевод которой мы сегодня публикуем, Эрик Гебельбекер, недавно собрал робота, основанного на одноплатном компьютере Raspberry Pi. Он хочет сделать так, чтобы роботом можно было бы управлять, пользуясь геймпадом. А для этого геймпад нужно подключить к Raspberry Pi. Эрик несколько лет назад публиковал статью, посвящённую решению этой задачи. Данный материал представляет собой обновлённый вариант той статьи.
Робот, основанный на Raspberry Pi, и геймпад
Предварительные требования
Для того чтобы воспроизвести то, о чём я хочу рассказать, вам понадобится Raspberry Pi и геймпад.
Здесь можно купить такой же набор для сборки робота, как у меня (это — партнёрская ссылка, как и некоторые другие).
Геймпад, которым пользуюсь я, Logitech F710 можно найти тут. Правда, пользоваться в точности таким же геймпадом вам необязательно. Мои инструкции подойдут для подключения к Raspberry Pi любого USB-геймпада. Вы можете столкнуться с другими скан-кодами, но, следуя моим инструкциям, сможете подстроить всё под себя.
Вы должны уметь работать с терминалом Raspberry Pi. О том, как с ним работать, а так же об установке Jupyter, можно почитать в этой моей публикации. Благодаря ей вы научитесь пользоваться терминалом и получите средства для испытания кода, который я вам покажу.
Если на вашем Raspberry Pi уже работает какая-то программа, наблюдающая за контроллерами, остановите её перед тем, как делать то, о чём я расскажу ниже.
Чтение данных из файлов устройств
Для того чтобы пользоваться геймпадом из программ, работающих на Raspberry Pi, нам сначала нужно прочитать информацию с этого геймпада. В Linux, как и в большинстве UNIX-подобных операционных систем, это — задача несложная, решаемая посредством работы с файлами устройств. Когда геймпад (или, в случае с беспроводными устройствами — ресивер) подключён к компьютеру, Linux создаёт особый файл, в который, при взаимодействии с геймпадом, попадают данные об этом.
Директория /dev/input
Давайте, для начала, не подключая к плате геймпад или ресивер, заглянем в директорию /dev/input
.
Для того чтобы это сделать, надо открыть терминал на Raspberry Pi. Сделать это можно либо подключившись к плате по SSH, либо — подключив к ней клавиатуру и дисплей. Тут мне хочется отметить, что у терминала Jupyter, по видимому, есть какие-то проблемы с чтением файлов устройств.
Теперь взглянем на содержимое директории /dev/input
, выполнив в терминале команду ls
.
Просмотр сведений о содержимом директории /dev/input
Вы должны увидеть тут файл mice
.
(Я не вижу тут никаких других файлов, так как к моему Raspberry Pi пока не подключено никаких устройств ввода. Вы, если к вашему компьютеру подключены мышь и клавиатура, можете увидеть тут ещё несколько файлов.)
Теперь подключите к плате геймпад или его ресивер и, подождав несколько секунд, снова поинтересуйтесь содержимым директории.
Изменения в содержимом директории /dev/input
Тут, если это — первое устройство, которое вы подключаете к плате, можно будет увидеть несколько новых файлов и пару новых директорий.
Моему геймпаду соответствуют файлы event0
и js0
. Директории by-id
и by-path
дают нам альтернативный метод адресации устройств, а так же — дополнительную информацию о них.
Так как устройства представлены файлами, для работы с ними можно использовать утилиты командной строки. Взглянем на то, что находится «внутри» нашего устройства.
Открывать эти файлы в обычном редакторе — не самая удачная идея. А вот старая добрая утилита cat
поможет нам увидеть кое-что интересное.
Попробуем команду cat event0
(у вас вместо 0
может быть какое-то другое число — в том случае, если к плате, до подключения геймпада, уже было что-то подключено) и нажмём на какую-нибудь кнопку геймпада.
Просмотр файла event0
То, что показано на предыдущем рисунке, соответствует нажатию на клавишу геймпада. Как видите, тут много каких-то символов. Некоторые из них выглядят совершенно бессмысленными. Но самое главное это то, что файлы, представляющие в системе геймпад, это то же самое, что и любые другие файлы, содержимое которых можно читать.
Чтение данных с геймпада
Для того чтобы интерпретировать данные, поступающие с геймпада, мы будем пользоваться пакетом evdev. О его возможностях мы поговорим по мере продвижения по материалу. Мне этот пакет, в предустановленном виде, не попадался ни на одном из Raspberry Pi, с которым мне доводилось работать. Поэтому мы установим его с помощью pip
:
sudo pip install evdev
Через некоторое время пакет будет установлен.
Теперь откроем геймпад и выведем некоторые сведения о нём. Тут я пользовался JupyterHub, но те же сведения можно получить и воспользовавшись командной строкой интерпретатора Python, и запустив соответствующий Python-скрипт.
from evdev import InputDevice
gamepad = InputDevice('/dev/input/event0')
print(gamepad)
Чтение сведений о геймпаде
В первой строке скрипта мы импортируем InputDevice
из evdev
.
Во второй строке создаётся объект gamepad
путём передачи InputDevice
пути к файлу устройства геймпада.
И мы, наконец, получаем довольно интересные сведения об этом объекте. Для остановки цикла чтения нужно либо нажать в Jupyter Stop
, либо воспользоваться сочетанием клавиш CTRL + C
.
Хотя механизм чтения данных из файлов, связанных с геймпадом, весьма прост, использование пакета evdev
упрощает всё ещё сильнее. В этом пакете содержатся огромные объёмы кода, ориентированного на работу с различными устройствами наподобие геймпадов и джойстиков.
Воспользуемся возможностями evdev
для чтения сведений о нажатиях на кнопки геймпада. Первые строки нашего кода будут, в сущности, такими же, как прежде, а вот дальше, вместо печати сведений о геймпаде, мы прочитаем с устройства сведения о событии, связанном с нажатием на кнопку.
from evdev import InputDevice, categorize, ecodes
gamepad = InputDevice('/dev/input/event0')
for event in gamepad.read_loop():
if event.type == ecodes.EV_KEY:
keyevent = categorize(event)
print(keyevent)
Чтение сведений о нажатии на кнопку
Теперь нам должны быть понятны причины того, что в файл event0
при нажатии на кнопку попадает много непонятных символов. Геймпад, при каждом нажатии на кнопку, отправляет на компьютер большой объём информации.
Прежде чем мы поговорим о событиях — остановимся подробнее на этой строке:
for event in gamepad.read_loop()
Она иллюстрирует использование одной из полезных возможностей пакета evdev
. Дело в том, что этот пакет даёт в наше распоряжение простой цикл, который считывает данные с устройства и создаёт события. Если бы пакет чем-то подобным не обладал — наш код мог бы выглядеть примерно так:
from evdev import InputDevice
from select import select
gamepad = InputDevice('/dev/input/event0')
while True:
r,w,x = select([gamepad], [], [])
for event in gamepad.read():
print(event)
При этом то, что вывелось бы на экран, выглядело бы далеко не так аккуратно, как в нашем случае.
Evdev
позволяет нам избавиться от внешнего цикла while
, а так же — от вызова select
. Если говорить о количестве строк кода в этом примере, и в том, где используются возможности evdev
, то можно сказать, что по длине они отличаются не особенно сильно. Но тот код, где применяется read_loop()
, легче читать.
В приложениях, которым нужно отслеживать состояние нескольких устройств ввода, например — мыши и клавиатуры, механизм read_loop()
не будет работать без настройки нескольких потоков и усложнения некоторых других механизмов. Использование же select()
хорошо подходит для взаимодействия с несколькими устройствами.
Теперь разберёмся с тем, какая именно кнопка была нажата на геймпаде.
from evdev import InputDevice, categorize, ecodes
gamepad = InputDevice('/dev/input/event0')
for event in gamepad.read_loop():
print(categorize(keyevent))
Исследование событий, происходящих при нажатии на кнопку
Будем проверять события, и выяснять, имеют ли они отношение к кнопке.
Если это и правда кнопка — categorize()
даст нам более подробные сведения о типе события.
Обратите внимание на то, что у нас имеются данные и о нажатии, и об отпускании кнопки.
Завершим разговор созданием программы, реагирующей на события, появляющиеся при нажатии на кнопки геймпада A-B-X-Y.
Именно с помощью этих кнопок, находящихся в правой части геймпада, я собираюсь управлять роботом. Вот как выглядят данные о нажатиях интересующих меня кнопок:
Button 'A' - key event at 1607808074.513679, 305 (['BTN_B', 'BTN_EAST']), down
Button 'X' - key event at 1607808091.133587, 304 (['BTN_A', 'BTN_GAMEPAD', 'BTN_SOUTH']), down
Button 'Y' - key event at 1607808172.285273, 307 (['BTN_NORTH', 'BTN_X']), down
Button 'B' - key event at 1607808188.589244, 306 (BTN_C), down
В нашем распоряжении оказываются скан-коды кнопок, коды кнопок, и сведения о том, нажата или отпущена кнопка. Видно, что у одной из кнопок имеется целых три кода кнопки. А ещё у одной — всего один код. При этом те данные, что мы получаем в событиях, не соответствуют подписям кнопок на контроллере!
Поэтому мы, чтобы понять, какая именно кнопка нажата, можем просто использовать скан-коды, не обращая внимания на коды кнопок.
Ваш геймпад может выдавать другие коды. Вы, исследовав его поведение, можете внести в предлагаемый мной код соответствующие изменения.
from evdev import InputDevice, categorize, ecodes, KeyEvent
gamepad = InputDevice('/dev/input/event0')
for event in gamepad.read_loop():
if event.type == ecodes.EV_KEY:
keyevent = categorize(event)
if keyevent.keystate == KeyEvent.key_down:
if keyevent.scancode == 305:
print('Back')
elif keyevent.scancode == 304:
print ('Left')
elif keyevent.scancode == 307:
print ('Forward')
elif keyevent.scancode == 306:
print ('Right')
Попробуйте запустить этот код у себя и проверить его, понажимав на кнопки геймпада. Вы вполне можете модифицировать этот код так, чтобы он мог бы обрабатывать не только события, возникающие при нажатиях на кнопки, но и события, возникающие при их отпускании. Ещё можно учитывать длительность нажатия кнопок.
Планируете ли вы применять геймпад в своих Raspberry Pi-проектах?