image

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

Мотивация


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

image

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

image

Несколько месяцев назад, мой читатель Кирилл Севостьянов с Хабра прислал мне 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!

image

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

image

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


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

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

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

image

Инжект событий я писал на 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.

image

Геймпад


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

image

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

image

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

image

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

#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;
            }
        }
    }
}

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

image

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

Заключение


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

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

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

Комментарии (20)


  1. bodyawm Автор
    24.05.2023 08:05
    +6

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

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


    1. bodyawm Автор
      24.05.2023 08:05
      +2

      Я тут кстати на другой площадке создавал опрос, стоит ли мне найти полуушатанную PS2 в неизвестном состоянии и попытаться восстановить:

      Но чет ту консоль, которую я хотел забрать, стащили у меня из под носа :( Но тематический контент по "фирменным" консолям с написанием приложений под них точно будет!


    1. lenz1986
      24.05.2023 08:05

      а смысл? кроме экрана там ничего больше и нет. Периферии грубо нет, доступ к gpio сложный, написание под него каких то приложений.... надо старые студии. единственное дешевый, но не более того.


      1. bodyawm Автор
        24.05.2023 08:05
        +5

        кроме экрана

        Экрана, готового звука с микшером, примитивного 2D-видеоускорителя, Bluetooth, сетевого стека, возможно даже USB-хоста (но не на этом девайсе). Не так уж и мало забесплатно или за 100 рублей, верно?)

        Касательно приложений: старые студии без проблем работают на свежих виндах, есть возможность писать на плюсах, ObjPas (порт FPC), C#/VB.NET. Вполне себе ничего!

        Про GPIO я отдельно расскажу потом.


        1. Rusrst
          24.05.2023 08:05
          +2

          Пишите статью!!!


    1. Earthsea
      24.05.2023 08:05
      +1

      например, терминал для управления умным домом, ту же игровую консоль или кастомную приборную панель в машину

      Постоянно включенная карта погоды, осадков и гроз от Яндекса в прихожей тоже была бы не лишней, причем прямо на входной двери. Конечно, можно по утрам смотреть ее в смартфоне. Но, по закону Мёрфи, обычно ливень надвигается на город именно в тот момент, когда не посмотрел ни на прогноз, ни даже в окно, не взял зонтик и очень легко оделся.


    1. strvv
      24.05.2023 08:05
      +1

      да, но на многих wince памяти как на пентиумах 90-начала 2000х — 16-32 мб. другое дело, что этого хватает, но! все современные библиотеки быстро летят в корзину! и это, наверное, правильно.
      на 100-200мгц мипс или арм-v4/v5 с 16 мб памяти (hpc2000, ce3.0, многие навигашки)


      1. bodyawm Автор
        24.05.2023 08:05
        +1

        Навител кажись на меньше чем 64 не работает. А там уже как-то можно попробовать разгуляться))


  1. dlinyj
    24.05.2023 08:05
    +2

    Крутая хабратортная статья, моё уважение! Подскажи пожалуйста, как собирать прогу под Андроид и как ты её запускал. В общем, интересна кухня. Можно просто ссылок на RTFM накидать.


    1. bodyawm Автор
      24.05.2023 08:05
      +3

      Спасибо!

      Собирать через Android Studio. Я ближе ко второй части выложу полные исходники: сейчас их нужно немного "причесать" для публичного доступа - добавить автозапуск при загрузке (ловить BOOT_COMPLETED, сейчас запускаю вручную с помощью встроенного дебаггера) и хэндлинг ошибок. Прошивку для RPi Pico можно собрать CMake'ом, не забыв добавить их SDK с тулчейном в Path


      1. dlinyj
        24.05.2023 08:05
        +1

        Интересно, интересно, ждём.


        Ну с RPi Pico примерно понятно.


  1. metter
    24.05.2023 08:05
    +2

    Для устройства андроид с неработающим тачскрином как вариант можно через otg мышку подключить


    1. bodyawm Автор
      24.05.2023 08:05
      +2

      Можно и просто HID-устройство реализовать на базе USB-стека ESP32 или того же Pico. Цель статьи была немного в другом:

      1. Показать, что с старыми устройствами вполне можно полноценно общаться на физическом уровне (т.е UART) и использовать их в качестве одноплатников (об этом расскажу в одной из следующих статей)/игровых консолей или ещё каких-то целей. Даже как одноплатник он офигенный: есть контроллер АКБ сразу с зарядкой, иногда можно выцепить 3.3в с КП, сразу есть дисплей, ЦАП для вывода звука, компактный USB-хост через OTG.

      2. Показать, что старые и казалось бы, закрытые планшеты вполне себе поддаются железному моддингу и им не место на мусорке.


      1. Sevo98
        24.05.2023 08:05
        +1

        Интересно было бы посмотреть на реализацию одноплатника) Планируешь ли GPIO к такому кастомному одноплатнику подключать?


        1. bodyawm Автор
          24.05.2023 08:05

          да, конечно, иначе какой это одноплатник)) Только одноплатник я сделаю из навигатора на WinCE (см. фото выше).


      1. action5
        24.05.2023 08:05
        +1

        а вот у меня то же желание только с платой от современных смарт тв. один на webos второй самсунговский. по сути одноплатиники. но закрытые, инфы совсем нет если не андройд.


        1. bodyawm Автор
          24.05.2023 08:05

          Поищите уарт на плате :)


  1. iamkisly
    24.05.2023 08:05

    Корпус намоделен уже? Какой город?


    1. bodyawm Автор
      24.05.2023 08:05

      Ейск. Корпус чутка позже смоделю, там несложная форма будет (по сути, сделаю что-то типа джойконов и приклеию их к основному корпусу планшета.


      1. iamkisly
        24.05.2023 08:05
        +1

        Почти рядом) Если будет желание и успеете смоделировать задолго до того как приобретёте принтер, напишите в лс или в vk. Возможно помогу с печатью.