Робот, основанный на 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-проектах?