Октодон: going deeper underground

в 7:12, , рубрики: Без рубрики

В этом очерке мне бы хотелось немного отойти от истории клавиатуры Октодон и рассказать о реальных проблемах, с которыми мы сталкивались на разных этапах разработки прототипов. Так выходит, что никогда не знаешь, насколько сложной будет стыковка высокоуровневого API (в нашем случае Android API) и самодельного железа. Подводные камни подстерегают буквально на каждом шагу, и никогда не можешь даже приблизительно оценить, насколько трудоёмка та или иная задача. Учитывая вдобавок, что время ограничено — всегда на носу какая-нибудь выставка или презентация, к которой прототип уже должен работать, решения приходится изыскивать всеми возможными способами

Октодон: going deeper underground
photo by Patrick Pleul/AFP/Getty Images

… и результат редко получается изящным.

Я участвую в создании Октодона как Android-разработчик, электронщик и радиомонтажник. Так получилось, что несмотря на то, что в последние несколько лет моя основная деятельность — это высокоуровневая разработка на .NET, детство моё было осчастливлено компьютером ZX Spectrum и, соответственно, ассемблером для процессора Z80. В результате я слишком рано стал понимать, как компьютер работает на уровне сигналов и временных диаграмм, и это не могло не наложить отпечатка — я до сих пор очень люблю низкоуровневую разработку. И когда мне предложили поучаствовать в проекте создания принципиально новой клавиатуры, “железки”, я просто не смог отказаться.

Хаками (а иначе и не назвать), про которые я сейчас расскажу, я совершенно не горжусь. Но они позволили проекту расти и развиваться, позволили иметь не голую идею, а рабочую реализацию, способную привлекать заинтересованных лиц, будущих пользователей и инвесторов. На нынешнем этапе развития весь этот код уже не используется, не волнуйтесь :)

Возможно, что для создания более полной картины того, чем мы вообще занимаемся, вам будет интересно прочитать предыдущий пост, написанный нашим предводителем AlexLysenko: Клавиатура Октодон в поисках Правильного Клика

Прототип №1 — Bluetooth

Наш первый прототип использовал обмен через Bluetooth. Казалось бы, не может быть более удобной технологии для соединения клавиатуры и телефона, и была уверенность, что всё заработает из коробки. Но этого не случилось.
Первая проблема возникла с тем, что, начиная с версии 2.3, в Андроиде появилась встроенная в систему поддержка BT-клавиатур. Как только система обнаруживала “спаренную” клавиатуру, то тут же захватывала её и начинала использовать как средство ввода. Поскольку то же самое собирались делать и мы, у нас неминуемо на одно нажатие джойстика Октодона печаталось бы два символа — “наш” и “системный”. Требовалось как-то приглушить излишне говорливого Андроида. Телефон был “рутован”, и началось копание в исходниках ядра и системных библиотек с целью найти наиболее удобное место, где можно было бы проигнорировать подключение клавиатуры. Таким местом оказалась библиотека libui.
В функцию open_device был внедрен грязный хак вида:

if ((id.vendor == 0x05AC && id.product == 0x0239)) // VID и PID нашей клавиатуры
{
    LOGI("Ignoring device %04x %04x", id.vendor, id.product);
    close(fd);
    fd = -1;
    return -1;
}

Как видно, мы просто не даём открыть наше устройство. Теперь ОС перестала отбирать у нас клавиатуру, и с ней стало возможно общаться, используя Bluetooth-функции Android API. И тут снова поджидала проблема: никак не удавалось открыть BT-сокет для приёма данных — стандартная для этого функция createRfcommSocketToServiceRecord падала с ошибкой. Анналы истории не сохранили подробностей, но проблема отняла очень много времени. В итоге дело дошло до реверс-инжиниринга продуктов из (тогда ещё) Маркета, позволявшим работать с этой BT-клавиатурой на “старых” Андроидах, которые не умели делать этого сами. И вот за баррикадами обфускации в одной из программ было найдено прекрасное:

private static BluetoothSocket createBluetoothSocket(int type, int fd, boolean auth, boolean encrypt, String address, int port)
    {
        try
        {
            Class[] parameterTypes = new Class[] {
                Integer.TYPE, // type
                Integer.TYPE, // fd
                Boolean.TYPE, // auth
                Boolean.TYPE, // encrypt
                String.class, // address
                Integer.TYPE  // port
            };
            
            Constructor<BluetoothSocket> constructor = BluetoothSocket.class.getDeclaredConstructor(parameterTypes);
            constructor.setAccessible(true);

            Object[] parameters = new Object[] {
                Integer.valueOf(type),
                Integer.valueOf(fd),
                Boolean.valueOf(auth),
                Boolean.valueOf(encrypt),
                address,
                Integer.valueOf(port),
            };
            
            return (BluetoothSocket)constructor.newInstance(parameters);
        }
        catch (Exception ex)
        {
            return null;
        }
    }


BluetoothSocket intrSocket = createBluetoothSocket(3 /* TYPE_L2CAP */, -1, false, false, "DC:2C:26:A0:C2:50" /* MAC-address */, 0x13 /* INTR port */);

Вызов private-конструктора типа! Удивление стало ещё большим, когда представленный кусок кода взял и сразу же заработал. Разбираться, что, как и почему уже не было времени, поэтому код был оставлен прямо в таком виде. В этом один из минусов низкоуровневой разработки — часто приходится смиряться с тем, что часть кода работает “магически”: никто не знает, почему оно работает, и никто не знает, почему более нормальное решение работать отказывается… К счастью, мы быстро ушли от этой Bluetooth-клавиатуры, сделав с её использованием только один прототип, поэтому от такой “магии” вскоре удалось избавиться.

Прототип №2 — USB HID-клавиатура

Как только стало ясно, что использование готовых устройств нас сильно ограничивает, было решено разрабатывать своё. Основным смартфоном, на который мы ориентировались, к этому времени стал Samsung Galaxy S2. У него был нормально и стабильно работающий USB-host, к которому мы и собирались подключиться. В качестве контроллера была взята Teensy 1.0. Очень не вовремя оказалось, что её возможностей нам мало, и что брать-то следовало более свежую версию, 2.0. Отличие было в библиотеках для создания HID-совместимых устройств. Если 1.0 умела прикидываться только HID-клавиатурой или мышкой, то на 2.0 можно было реализовать гораздо более универсальное устройство Raw HID, использующее наш собственный протокол. Но что поделать, сперва пришлось довольствоваться тем, что было. При эмуляции HID-клавиатуры снова возникла описанная выше проблема — при подключении система сама захватывала её как устройство ввода, и посылаемые символы печатались два раза — нами и ОС.

Описанное же выше решение уже не годилось — решили, что с нас хватит изменений системы. Если мы хотим создать устройство, которое можно выпустить на рынок, то оно должно работать на любом Galaxy S2, не должно быть необходимости в перепрошивке.

В процессе экспериментов с обычной USB-клавиатурой, подключенной к S2, выяснилось, что смартфон почему-то не реагирует на нажатия кнопок в цифровой части клавиатуры. Это стало серьёзным подспорьем, ведь наша Teensy могла каким-то образом кодировать отклонения джойстиков Октодона в нажатия цифровых кнопок. На каждое изменение состояния джойстиков мы стали передавать посылки из 3 байт:

  1. Сканкод “Num 0”...”Num 9” — номер джойстика, статус которого передаём.
  2. Сканкод “Num 0”…”Num 4” — направление отклонения джойстика. 0 — влево, 1 — вниз, …, 4 — центральное нажатие.
  3. “Num +” если было нажатие или “Num -” если отпускание.

Таким образом, простое нажатие первого джойстика влево давало нам последовательность “00+”, отпускание — “00-”. Не отпуская один джойстик, можно отклонять второй — таким образом можно кодировать одновременные нажатия нескольких кнопок, что нам было очень нужно для Shift-режима.

Тем не менее, стояла ещё задача каким-то образом получить переданные сканкоды. API системы их игнорировало, поэтому пришлось лезть глубже. К нашему великому счастью, Android — это *nix, и тут всё — файл. Нужный файл оказался в /dev/input и в зависимости от расположения звёзд на небе назывался или event7, или event8. Доступ к нему был только у root, поэтому устройство пришлось-таки рутовать, но этим всё и ограничилось — перепрошивка не понадобилась. Само использование root в Android — интересная задачка, так как какого-либо способа получить соответствующие права через API нет (или нам не удалось найти). Поэтому выполняем всё прямо через терминал:

Process root = Runtime.getRuntime().exec("su"); // запускаем команду su
DataOutputStream shellStream = new DataOutputStream(root.getOutputStream());
shellStream.writeBytes("cat /dev/input/event7 & && cat /dev/input/event8 &"); // запускаем в бэкграунде две команды, копирующие в терминал всё, что пошлёт устройство
InputStream inputStream = root.getInputStream();
InputStream errorStream = root.getErrorStream();

// читаем 16-байтные данные от устройства
byte[] buf = new byte[16];
if (inputStream.available() > 15)
{
    int data = inputStream.read(buf, 0, 16);
}

Прототип, работавший таким способом, просуществовал долгое время, прошёл множество выставок, и жив и поныне. Со временем код облагородился, и имена файлов устройств вместо хардкода стали задаваться через конфиг-файл.

Ну а дальше настал золотой век. Для Galaxy S2 вышел 4-й Android, а к нам приехали заказанные Teensy 2.0. Стыковка разработанного на базе нового контроллера Raw HID устройства с появившимся в новой ОС USB Host API прошла настолько гладко, что в рамки данного очерка, призванного расказывать о проблемах, совсем не укладывается. Сбылась давняя мечта, Октодон смог работать на “стоковом” смартфоне без необходимости делать с ним что бы то ни было. Мы получили возможность выпускать мелкосерийные образцы, которые просто работают, и их не требуется доводить до ума напильником. Но красивые решения без костылей — это тема для отдельной статьи, тем более что на страницах Хабра о работе с USB Host API не рассказывалось, кажется, никогда. Так что продолжение следует!

Автор: Eltaron

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js