Предварительные требования
Для того чтобы воспроизвести то, о чём я хочу рассказать, вам понадобится 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
.
Вы должны увидеть тут файл
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')
Попробуйте запустить этот код у себя и проверить его, понажимав на кнопки геймпада. Вы вполне можете модифицировать этот код так, чтобы он мог бы обрабатывать не только события, возникающие при нажатиях на кнопки, но и события, возникающие при их отпускании. Ещё можно учитывать длительность нажатия кнопок.