Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

К сожалению, в наше время многие старые, но весьма неплохие по характеристикам гаджеты отправляются напрямую в помойку, и их владельцы не подозревают, что им можно найти применение. Сервер, мультимедийная-станция, да даже просто как TV-приставка — люди в упор не замечают сфер, где старенький планшет мог бы быть полезен. Но как быть, если посвящаешь жизнь портативным гаджетам, кодингу и копанию в железе? Правильно: сделать довольно мощную игровую консоль из старого планшета самому! Сегодня вам расскажу, как я сделал свою портативную приставку из планшета с нерабочим тачскрином, Raspberry Pi Pico и 8 кнопок! За рабочим результатом прячется несколько дней работы: поиск UART на плате, разработка контроллера геймпада на базе RPi Pico, написание приложения-сервиса, которое слушает события и отправляет их в подсистему ввода Linux в обход Android. Интересно? Тогда жду вас под катом!

❯ Мотивация

Прошло уже практически 10 лет с того момента, как у меня появилась моя первая портативная консоль. Несмотря на то, что я был заядлым ПК-игроком, я уже успел посмотреть на PS3 и PSP, но денег на их покупку у меня особо не было, да и к тому времени уже был в наличии Android-планшет. Но к моему 13-летию в 2014 году, когда я ходил и выбирал себе будущий девайс на день рождения, отец и мама решили подарить мне мою первую портативную консоль. Изначально, я уговаривал её купить мне целых два девайса, но бюджет был ограничен 4.000 рублей, а я хотел взять смартфон Fly IQ239 и консоль JXD S601 одновременно:

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

Однако, увидев здоровую 7-дюймовую консоль в магазине TREC (думаю, жители южной части РФ помнят такой), мама уговорила меня взять именно её, мотивируя это «ну и чего ты будешь тыкаться в этот мелкий экран? Возьми большую». После покупки гаджета, я был доволен: играл какие-то игрушки с ретро-платформ, устанавливал игры на Android, сидел в ВК через Kate Mobile. Что еще нужно было школяру? Однако, планшет прожил у меня недолго: с очередного лага я психанул и ударил по нему кулачком, унеся на тот свет и дисплей и тачскрин. Так консолька и пролежала в подвале около 8 лет. Впрочем, мне продолжали импонировать подобные устройства и в прошлом году я купил и написал про несколько подобных девайсов.

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

Несколько месяцев назад, мой читатель Кирилл Севостьянов с Хабра прислал мне HTC HD2 в качестве донора и планшет Prestigio PMP7170B3G, который был рабочим, но… у него отказал тачскрин. Я всё думал, чего бы с ним сделать и решил реализовать игровую консольку своими руками из подручных средств. Идея крутилась в голове довольно давно, но реализовал я её только сейчас.

❯ Что нам нужно сделать?

Итак, что должно быть у портативной консоли? Чипсет, дисплей, звук, ОС — это всё нам уже предоставляет планшет. Нам остаётся лишь сделать свой геймпад. Давайте подумаем, что нам будет нужно для того, чтобы его сделать и передавать от него события на планшет:

  • Контроллер для геймпада: тут нам подойдет практически любой микроконтроллер, который работает от 3.3в. Выбор большой: Arduino Pro Mini 3.3v, ESP32, RPi Pico. Я остановился на последнем: недавно я взял себе две штучки «пощупать» их — и они мне очень понравились!
  • Физический интерфейс: с планшетом нужно как-то общаться. У нас есть три варианта: USB (не факт, что поддержка преобразователей включена в ядре), UART и SPI/I2C на пятачках тачскрина (потребуют написания драйвера т. к. в android-устройствах нет прямого доступа к SPI/I2C из userland'а). Я остановился на UART: его легко найти на большинстве китайских планшетов, а если не получилось — то на помощь может прийти схема платы.
  • Программная реализация: как это будет работать? Я решил реализовать геймпад в виде сервиса на Android, который слушает состояния кнопок с UART и «инжектит» события напрямую в драйвер ввода. Таким образом, поддержка нашего геймпада появляется даже в самой системе — можно управлять менюшкой или приложениями как с клавиатуры!

С планом определились, пора начать с программной части: сначала нам обязательно понадобится ROOT-доступ. Его получение на разных девайсах отличается — на prestigio уже был порт CWM и я просто поставил SuperSU. Без ROOT доступа мы не сможем использовать UART!

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

Теперь нам нужно найти пятачки UART на плате. Разведен он не везде, но в случае устройств на MediaTek — почти всегда, ещё и пятачки подписаны. На моём планшете он нашёлся сразу: был между двух металлических экранов и соответствовал 4-ому каналу UART. Получить к нему доступ можно в /dev/ttyMT3. Я использую ESP32 в качестве UART преобразователя: подпаиваемся к RX/TX, запускаем putty и заходим в adb shell. Определяем бодрейт (скорость) нашего UART порта — на MediaTek он обычно равен 921600, на других чипсетах — 115200. Пытаемся что-то вывести и хоба — мы уже можем «поболтать» с планшетом!

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

❯ Приложение-сервис

Итак, у нас уже есть доступ к UART и мы можем общаться с планшетом из внешнего мира. Но получить события с кнопок пол дела, нужно их ещё и послать в систему. Для этого есть целых три способа:

  • InputManager.injectInputEvent — именно этим методом пользуется команда input, которую вы можете использовать через adb. Но увы, он работает только при наличие разрешения INJECT_EVENTS, который доступен только системным приложениям — находятся они в /system/app и подписаны тем же сертификатом, что и остальная прошивка.
  • Модуль uinput дает возможность создать виртуальное устройство ввода и посылать события из userland'а — т. е. из прикладного приложения. У моего планшета было устройство /dev/uinput, но lsmod показывал, что сам модуль не загружен. Так что отметаем — он есть не везде.
  • Прямой инжект событий в character устройство — весьма грязный хак, который позволяет инжектить события, не притворяясь системным приложением, но имеет некоторые ограничения. Именно его я и выбрал и о ограничениях ниже.

Сначала нам нужно узнать, какие кнопки поддерживают загруженные устройства ввода в системе. Для этого используем команду getevent -li. Там есть разные устройства ввода, в том числе и тачскрин (если вам нужно симулировать нажатия на экран), мне же подошёл драйвер физических кнопок mtk-kpd. Он занимается обработкой кнопок громкости, включения и т. п. Тут важно обратить внимание на то, что если попытаться послать кнопку, которое устройство не реализует (например пробел), то ничего не произойдет:

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

Инжект событий я писал на C, т. к. это требовало прямой записи input_event, а в Java прокинул его через Jni. Концепция простая: открываем устройство /dev/input/event2 и посылаем в него события ввода и синхронизации (это обязательно!), которые затем Android читает и обрабатывает:

#include <linux/uinput.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <android/log.h> #include <jni.h> int uinput; extern "C" JNIEXPORT void JNICALL Java_com_monobogdan_inputservicebridge_InputNative_init(JNIEnv *env, jclass clazz) { uinput = open("/dev/input/event2", O_WRONLY); __android_log_print(ANDROID_LOG_DEBUG , "Test", uinput >= 0 ? "Open event OK" : "Failed to open event"); } void emit(int fd, int type, int code, int val) { struct input_event ie; ie.type = type; ie.code = code; ie.value = val; /* timestamp values below are ignored */ ie.time.tv_sec = 0; ie.time.tv_usec = 0; write(fd, &ie, sizeof(ie)); } extern "C" JNIEXPORT void JNICALL Java_com_monobogdan_inputservicebridge_InputNative_sendKeyEvent(JNIEnv *env, jclass clazz, jint key_code, jboolean pressed) { __android_log_print(ANDROID_LOG_DEBUG , "Test", "Send"); emit(uinput, EV_KEY, key_code, (bool)pressed ? 1 : 0); emit(uinput, EV_SYN, SYN_REPORT, 0); }

Основной обработкой занимается сервис, который я реализовал в отдельном потоке: он слушает события с UART и посылает соответствующие изменения состояния через sendKeyEvent. На вход приходят простые сообщения вида:

U L где U/D — нажато, не нажато, а L — однобайтовый идентификатор кнопки. В случае L — это влево, R — вправо и т. п. Вся доступная раскладка хранится в словаре. Причём само чтение из UART реализовано костылем с чтением «чужого» stdout, т. к. android-приложения не умеют сами по себе работать с root правами. В теории, это могло дать неприятный оверхед, но на практике никакого серьезного инпут лага это не создает. Не забываем сделать устройство event записываемым — ставим ему права 777:

package com.monobogdan.inputservicebridge; public class InputListener extends Service { private static final int tty = 3; private InputManager iManager; private Map<Character, Integer> keyMap; private Method injectMethod; private Process runAsRoot(String cmd) { try { return Runtime.getRuntime().exec(new String[] { "su", "-c", cmd }); } catch (IOException e) { e.printStackTrace(); return null; } } @Override public void onCreate() { super.onCreate(); // According to linux key map (input-event-codes.h) keyMap = new HashMap<>(); keyMap.put('U', 103); keyMap.put('D', 108); keyMap.put('L', 105); keyMap.put('R', 106); keyMap.put('E', 115); keyMap.put('B', 158); keyMap.put('A', 232); keyMap.put('C', 212); InputNative.init(); try { runAsRoot("chmod 777 /dev/input/event2").waitFor(); } catch (InterruptedException e) { throw new RuntimeException(e); } Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { Process proc = runAsRoot("cat /dev/ttyMT" + tty); BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); while(true) { try { String line = reader.readLine(); if(line != null && line.length() > 0) { Log.i("Hi", "run: " + line); boolean pressing = line.charAt(0) == 'D'; int keyCode = keyMap.get(line.charAt(2)); Log.i("TAG", "run: " + keyCode); InputNative.sendKeyEvent(keyCode, pressing); } } catch(IOException e) { e.printStackTrace(); } /*try { Thread.sleep(1000 / 30); } catch (InterruptedException e) { e.printStackTrace(); }*/ } } }); } @Override public IBinder onBind(Intent intent) { return null; } }

Таким образом, если мы отправляем с ПК «D L» — система считает, что мы зажали стрелку влево, а U L — считает что мы отпустили. Но если mtk-kpd поддерживает стрелки и еще некоторые действия без каких либо проблем, то enter в список обрабатываемых кнопок не входит: придется мудрить! И тут нам приходит на помощь механизм трансляции кодов кнопок в действия: они хранятся в специальных файлах .kl в /system/usr/keylayout/. Я назначил DPAD_CENTER на… кнопку регулировки громкости звука! Ну, а почему бы и нет. :) Таким образом можно переназначить уже имеющиеся кнопки громкости на, например, start/select.

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

❯ Геймпад

После того, как сервис был готов и отлажен, нужно было реализовать хардварную часть проекта — сам геймпад. В качестве контроллера я, как уже говорил, выбрал Raspberry Pi Pico на базе МК RP2040 — бодреньком контроллере с двумя ARM Cortex-M0 ядрами. Стоит копейки, а в отличии от ESP'шек, его SDK не такое перегруженное и выглядит более приближенным к bare-metal.

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

На данный момент, я решил развести все кнопки на бредборде — макетной плате без пайки, т. к. макеток для пайки у меня под рукой не было. Сделал примитивный геймпад:

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

Развел на соответствующие GPIO:

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

И написал примитивную прошивку, которая отслеживает состояние кнопок. В прошивке точно так же есть словарь, задающий ассоциацию между физическими пинами и «виртуальными» кнопками. При нажатии или отжатии кнопки, программа изменяет стейт и отсылает новое состояние планшету.

#include <stdio.h> #include <stdlib.h> #include <string.h> #include "pico/stdlib.h" #include "pico/time.h" #include "hardware/uart.h" struct keyMap { int gpio; char key; bool pressed; int lastTick; }; keyMap keys[] = { { 15, 'L', false, 0 }, { 14, 'U', false, 0 }, { 13, 'D', false, 0 }, { 12, 'R', false, 0 }, { 11, 'E', false, 0 }, { 10, 'B', false, 0 }, { 20, 'A', false, 0 }, { 21, 'C', false, 0 } }; #define KEY_NUM 8 int main() { stdio_init_all(); uart_init(uart0, 921600); gpio_set_function(PICO_DEFAULT_UART_TX_PIN, GPIO_FUNC_UART); gpio_set_function(PICO_DEFAULT_UART_RX_PIN, GPIO_FUNC_UART); sleep_ms(1000); // Allow serial monitor to settle for(int i = 0; i < KEY_NUM; i++) { gpio_init(keys[i].gpio); gpio_set_dir(keys[i].gpio, false); gpio_pull_up(keys[i].gpio); } while(true) { int now = time_us_32(); for(int i = 0; i < KEY_NUM; i++) { char buf[5]; buf[1] = ' '; buf[3] = '\n'; buf[4] = 0; if(!gpio_get(keys[i].gpio) && !keys[i].pressed && now - keys[i].lastTick > 15500) { buf[0] = 'D'; buf[2] = keys[i].key; puts(buf); keys[i].lastTick = now; keys[i].pressed = true; continue; } if(gpio_get(keys[i].gpio) && keys[i].pressed && now - keys[i].lastTick > 15500) { buf[0] = 'U'; buf[2] = keys[i].key; puts(buf); keys[i].pressed = false; keys[i].lastTick = now; } } } }

Собираем всё вместе и тестируем. Хоба, всё работает, мы можем перемещаться по менюшке используя наш геймпад!

Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера

А почему бы не попробовать поиграть в какую-нибудь игру? Ну мы же консоль вроде делаем: берём эмулятор NES, биндим кнопки в настройках и наслаждаемся игрой в Марио!

❯ Заключение

Реализация этого проекта заняла у меня не так уж и много времени: всего около 3-х дней работы по вечерам. Вероятно кто-то спросит: «а чего ты просто Bluetooth геймпад не купил?». Так это не прикольно ведь. Гораздо приятнее играть в девайс, к которому ты приложил руку сам. Более того, не у всех старых планшетов есть BT. Обошёлся на данной стадии проект недорого: планшет мне подарили бесплатно (точно также у вас дома может лежать подобный), RPi Pico — 350 рублей, кнопки по 10 рублей/штучка.

В целом, я сам по себе обожаю копаться в различных железках и их софтварной части (вспомнить хотя-бы статью про перекомпиляциюu-boot из вендорских исходников для нонейм консоли), а созидать что-то свое вообще вызывает какие-то нереальные всплески эндорфина — оно и понятно! :)

Однако несмотря на то, что мы уже имеем рабочий «прототип», проект далёк от завершения: я намерен довести его до конца и окончательно перевоплотить старый планшет в автономную игровую консоль (и рассказать об этом во второй части статьи). Для этого мне понадобится распечатать корпус и кнопки на 3D-принтере. К сожалению, у меня в городе ни у кого особо нет 3D-принтеров, поэтому начну копить на Ender 3, а от вас, читателей, с удовольствием почитаю мнение в комментариях и советы касательно выбора принтера!

Прикольный проект?
Да, занимательно было почитать. И главное — интересно его делать!
Нет, вообще не прикольный. Я бы купил BT геймпад и не морочил голову.
Автор наркоман с такими задумками
Если я выложу полную схему, исходный код под несколько разных МК и сервиса, собрали бы себе консоль?
Возможно на досуге и попробовал бы!
Нет, на такое интересно наблюдать со стороны.
А я не знаю!

Статья подготовлена при поддержке TimeWeb Cloud. Подписывайтесь на меня и TimeWeb на DTF, чтобы не пропускать еженедельные статьи про моддинг и различные недорогие девайсы каждую неделю!

5656
14 комментариев

У меня лежал роутер неплохой, но не грузился, я его по ком-порту через телнет прошил, потратил на это пару дней куря гайды, и было счастье, когда он запустился, ловил 4G, раздавал, летал, но оно было недолгим, через 2 недели он опять завис и я расхуярил его молотком.

4

Друзья! Я потихоньку развиваю рубрику серьезного хардварного моддинга у себя в статьях. Именно поэтому я хотел бы купить себе 3D-принтер: в голове есть довольно много интересных идей.

Из ближайших: изначально героем статьи мог стать вот этот навигатор на WinCE. Большинству людей они сейчас абсолютно не нужны и их часто можно найти на свалках или в шкафах, ожидающими своего часа. А ведь между прочим, эти устройства вполне себе можно попробовать превратить в неплохой одноплатник, да ещё и с привычным (для некоторых) WinAPI. Внешних интерфейсов там два: два канала UART (один под логи, нельзя открыть из под системы на чтение) и один для GPS (сам гпс физически расположен в чипе, из-за чего его нельзя "выпаять") и Bluetooth. Из такого девайса можно сделать много чего: например, терминал для управления умным домом, ту же игровую консоль или кастомную приборную панель в машину. Отдельной крутой фишкой я считаю тотальную совместимость дисплеев: почти все дисплеи автонавигаторов легко взаимозаменяются и имеют идентичный интерфейс (TTL RGB) и распиновку

1

Да не репостим мы в якорь такое

Что делать, если в первом опросе все три варианта подходят?)
Спасибо, почитал с интересом

1

Техномаг блин......

1