Автор статьи, перевод которой мы сегодня публикуем, Эрик Гебельбекер, недавно собрал робота, основанного на одноплатном компьютере 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-проектах?