Подключаем к PSP геймпад от Xbox 360 при помощи Raspberry Pi

в 11:04, , рубрики: Без рубрики

… или сказ про то, как пингвин Америку с Японией подружил.

Подключаем к PSP геймпад от Xbox 360 при помощи Raspberry Pi

Итак, в стародавние времена люди играли в Sony Playstation и их все устраивало. Но прогресс не стоял на месте. Увеличивалась степень интеграции микросхем. Инженерная мысль постоянно искала новые формфакторы, а мысль маркетинговая — новые рынки сбыта. Так в 2005 году появилась в продаже за пределами Японии портативная игровая система Sony Playstation Portable. Ее игровая линейка (как и у любой другой приставки) представляла собою специально скомпилированные под железо PSP игры. Но также она обладала и достаточными вычислительными мощностями для запуска игр от оригинальной PlayStation через встроенный эмулятор. Хотя, быть может, тут ключевую роль сыграло то, что и у PlayStation, и у PSP стоял процессор одной и той же архитектуры — а именно MIPS. Но самое примечательное в этой системе то, что сразу же, в год запуска, в сеть утекли библиотеки PSP SDK. В результате, через почти 10 лет с момента запуска, мы имеем огромную библиотеку игр и отлаженного homebrew. Также, сейчас, в не самом крупном городе России полностью рабочую PSP (самой функциональной модификации) можно купить с рук за 3000 рублей. Все это делает ее к настоящему моменту очень привлекательной бюджетной игровой системой с просто огромной инсталлбазой. Самая функциональная модификация имеет компонентный выход для подключения к ТВ. Но разъем подключения к ТВ располагается неудачно с точки зрения долгого использования PSP в качестве геймпада. К тому же, при длительном использовании возникает необходимость подключения второго провода — от зарядного устройства. И удобство использования такой химеры стремится к нулю. Как относительно бюджетно и при этом гиково решить эту проблемку — про это и пойдет речь в этой статье. Также вкратце будут затронуты темы программирования драйвера USB-клиента под PSP, методика установки хук-функций в PSP, работа под линуксом с устройствами USB и с джойстиками через API. Мы начинаем.

Идея подключения портативной консоли к ТВ не нова

Но прежде чем начнем, расскажу об одном интересном факте. Поскольку PSP вышла почти 10 лет назад, то на данный момент актуальным является следующее поколение портативных консолей от Sony, а именно Sony Playstation Vita. И дело в том, что в Японии вышла стационарная версия портативной консоли. Sony PlayStation Vita TV.

PS Vita TV

Подключаем к PSP геймпад от Xbox 360 при помощи Raspberry Pi

В качестве геймпадов она использует обычные Dualshock 3 от Playstation 3. Поддеживается USB или Bluetooth подключение. Vita TV, как и Vita, может воспроизводить игры от Vita, PSP и от оригинальной Playstation. Таким образом, идея «стационарной портативной» консоли достаточно состоятельна и интересна.

С помощью чего подружить геймпад и PSP?

Далее возник вопрос, как подключить внешний геймпад к PSP. Казалось бы, PSP имеет разъем USB, через который энтузиасты научили PSP запускать игры из папки подключенного компьютера или передавать всю картинку с игрой в окно этого самого подключенного по USB компьютера. Но, как оказалось, USB в PSP может быть только клиентом. И даже официальные аксессуары (например камера) работают в режиме хоста (кстати гугл при работе с периферией в андроид рекомендует переводить смартфон также в режим клиента). Т.е. подключать геймпад к PSP напрямую бесполезно. Поэтому нужно какое-то промежуточное устройство. В местном радиоэлектронном магазине отладочные платы разной степени крутизны стоили от 1 до 10 тысяч рублей. При том что это микроконтроллеры, и о USB-хосте нужно думать отдельно. Тут на глаза попался Raspberry Pi.

Raspberry Pi

Подключаем к PSP геймпад от Xbox 360 при помощи Raspberry Pi

В этой машинке есть все, что нужно — 2 USB порта, а также полноценный Linux. В не самом крупном городе России старшая модель (с 512 Мб памяти и Ethernet) стоит 1500 рублей через доски бесплатных частных объявлений. Цена соизмерима с самыми дешевыми контролерными отладочными платами, а функциональность не в пример больше. Причем еще и «Made in the UK».

Начало исследования

Если просто подключить USB-кабель в PSP, то она станет видна как флешка. Нам же нужно, чтобы она принимала команды о внешнем управлении. Т.е. в PSP должен крутиться какой-то код, который будет принимать информацию по USB, и имитировать нажатие органов управления на самом PSP. Сама возможность запуска какого-то кода помимо лицензионных игр возможна только на пиратских прошивках. Технически пиратская прошивка — это программа, прикидывающаяся официальной программой, располагающейся на карте памяти, и которая при запуске подменяет в оперативной памяти рабочий код прошивки на модифицированный, позволяющий запускать игры из .iso-файлов с карты памяти PSP. Таким образом, прошивка работает ровно до следующей перезагрузки PSP. Но нам важно не это, а то, что она поддерживает плагины. Плагины — это объектные файлы, слинкованные в определенном формате, которые стартуют в отдельных потоках параллельно запуску главного меню, игр PSP или игр оригинальной Playstation. Последней версией оригинальной прошивки PSP является аж 6.60. Плагины, заточенные под более ранние версии прошивки могут не работать под последней прошивкой. Так произошло и в данном случае. Плагин, который умеет пересылать по USB от PSP к ПК под управлением Windows видео всего, что происходит на PSP и принимать данные от геймпада, подключенного к ПК в PSP, по USB, на прошивке 6.60 работал только наполовину, т.е. данные от геймпада до PSP доходили, но имитация управления органами управления PSP не работала. Я начал искать плагины, которые так или иначе работают с управлением именно на прошивке 6.60. И нашел. Другой плагин служит для работы с аналоговым стиком PSP, и он работает на последней прошивке. Все исходники для PSP компилируются вот этим homebrew SDK.

Модификация исходников плагинов PSP. Хуки.

За основу проекта плагина, который я буду модифицировать, я выбрал тот, что уже содержал работащий код USB-клиента. Но для привычной отладки и вообще уютной атмосферы мне понадобился printf(). На PSP. В выбранном плагине его не было. Зато в плагине, из которого я хотел вытащить рабочий код перехвата событий органов управления PSP он был, выполненный через перехват функции отрисовки очередного кадра в кадровый буфер и дорисовки к кадру нужных мне отладочных строк. Сам перехват функции (хук) отрисовки реализован следующим образом:

#define GET_JUMP_TARGET_(x) (0x80000000 | (((x) & 0x03FFFFFF) << 2))
int (*g_setframebuf)(int unk, void* addr, int width, int psm, int sync);
int setframebuf_hook_func(int unk, void* addr, int width, int psm, int sync)
{
	if(g_info == 1)
	{
		dbgprint( debugmsg, addr, psm );
		if (!g_info) DEBUG_RESET()
	}
	
	return g_setframebuf(unk, addr, width, psm, sync);
}

int hook_function(unsigned int* jump, void* hook, unsigned int* result)
{
	unsigned int target;
	unsigned int func;
	int inst;

	target = GET_JUMP_TARGET_(*jump);
	while (((inst = _lw(target+4)) & ~0x03FFFFFF) != 0x0C000000)	// search next JAL instruction
		target += 4;

	if((inst & ~0x03FFFFFF) != 0x0C000000)
	{
		printf("invalid!n");
		return 1;
	}

	*result = GET_JUMP_TARGET_(inst);
	func = (unsigned int) hook;
	func = (func & 0x0FFFFFFF) >> 2;
	_sw(0x0C000000 | func, target+4);

	return 0;
}

int module_start( SceSize args, void *argp )
{
	//...
	hook_function( (unsigned int*) sceDisplaySetFrameBuf, setframebuf_hook_func, (unsigned int*)&g_setframebuf );
	//...
}

После вызова hook_function() операционная система PSP при вызове своей внутренней функции ядра sceDisplaySetFrameBuf() будет фактически вызывать setframebuf_hook_func(). А для вызова оригинальной sceDisplaySetFrameBuf() нужно теперь вызывать g_setframebuf(). Кому интересна тема хуков, более подробно можно почитать например здесь.

Модификация исходников плагинов PSP. Управление.

Далее я добавил в модифицируемый проект рабочие хуки на функции управления sceCtrlReadBufferPositive(), sceCtrlPeekBufferPositive(), sceCtrlReadBufferNegative() и sceCtrlPeekBufferNegative(), взяв их из того же JoySens. Только сделал так, чтобы входными данными внутри них были последние присланные в PSP данные о состоянии геймпада, подключенного к ПК-хосту. Вот архив со всеми нужными бинарниками и исходниками. Перед запуском ПК-части программы нужно установить драйвера USB. Сначала нужно запустить плагин на PSP (как запустить плагин можно узнать, погуглив в яндексе). Потом перезагрузить PSP и подключить ее к ПК. Должно обнаружиться устройство PSP Type B. Далее скачиваем драйвера. Устанавливаем драйвера через мастер (bininf-wizard.exe), указывая наше устройство PSP Type B и говоря в конце установить драйвер.

Подготовка минимальной версии PSP-части

Все бы хорошо, но в сети есть исходники только на версию 0.19 RemoteJoyLite. А она некорректно работает на некоторых играх (например K-On! дико тормозит, а в Dungeon Siege появляются графические артефакты). В версии 0.20 это, как говорят, исправили, но исходников этой версии в открытом доступе нет. Поэтому было решено модифицировать протокол данных, передающихся по USB, чтобы передавать только минимум информации о состоянии геймпада, а также минимизировать размер исходника PSP-части. Из протокола были удалены все данные, передающиеся от PSP в ПК, и оставлена только одна структура, передающаяся от ПК в PSP, в результате чего тормоза и артефакты канули в небытие:

#define USBDATA_PATTERN 0x1234ABFE
struct 
{
	unsigned int Pattern;
	unsigned int ButtonData;
	unsigned int AnalogX;
	unsigned int AnalogY;
} PSPUsbData;

Аналоговые данные со стика в самой PSP представлены в виде однобайтового беззнакового целого для каждой оси (127 — центр), а 4 байта в протоколе выделено из-за желания даже не думать про проблемы упаковывания и выравнивания структур, т.к. в самом RemoteJoyLite данные упаковываются следующим образом:

struct HostFsCmd
{
	uint32_t magic;
	uint32_t command;
	uint32_t extralen;
} __attribute__((packed));

И чтобы даже не задумываться о соответствующих проблемах (ведь сам RemoteJoyLite компилируется MinGW GCC, а следующим шагом будет создание проекта в Microsoft Visual Studio для отработки урезанного протокола, которая про __attribute__((packed)) не знает), я убрал все выравнивания и упаковывания структур, приведя их к 32-битному представлению. В итоге, в этом архиве содержатся исходники и бинарники урезанного проекта — и PSP-, и Windows-часть. Windows-приложение выполнено в виде проекта mfc на C++ для Microsoft Visual Studio 2010. Изучающим Windows-программирование будет полезно посмотреть, как реализована обработка нажатий клавиш управления курсором (в mfc-приложениях оно работает, только если находится в фильтре оконных сообщений всего приложения, а не в диалоговом обработчике нажатия клавиш), а также за счет чего корректно работает printf() на форму даже из отдельного потока. В части же PSP будет интересно изучить прилагающийся легковесный исходник, в котором оставлены только USB, управление и printf(). Например, прием данных по USB асинхронен и реализован следующим образом. При подключении к USB-хосту, после корректной процедуры получения внутреннего адреса на шине USB (см. описание USB) происходит вызов UsbAttach(), поскольку в зарегистрированной при инициализации структуре драйвера был описан этот вызов:

#define RJLITE_DRIVERNAME "RJLiteDriver"
#define RJLITE_DRIVERPID  (0x1C9)
struct UsbDriver UsbDriver = {
	RJLITE_DRIVERNAME,
	4,
	UsbEndpoint,
	&UsbInterface,
	&UsbData[0].devdesc[0],
	&UsbData[0].config,
	&UsbData[1].devdesc[0],
	&UsbData[1].config,
	&StringDescriptor,
	UsbRequest,
	UsbUnknown,
	UsbAttach,
	UsbDetach,
	0,
	UsbStartFunc,
	UsbStopFunc,
	NULL
};

int module_start( SceSize args, void *argp )
{
	//...
	sceUsbbdRegister(&UsbDriver);
	if((sceUsbStart(PSP_USBBUS_DRIVERNAME, 0, 0) == 0) && (sceUsbStart(RJLITE_DRIVERNAME, 0, 0) == 0) && //...
	{
		//...
	}
	//...
}

Где module_start() — функция, вызываемая в отдельном потоке при запуске плагина пиратской прошивкой. Также при запуске драйвера происходит создание флаговой переменной типа int (т.е. 32 флага) с доступом через уникальный идентификатор объекта в операционной системе PSP и запуск вспомогательного потока:

static SceUID UsbMainEventFlag = -1;
static int UsbStartFunc( int size, void *p )
{
	//...
	UsbMainEventFlag = sceKernelCreateEventFlag( "USBMainEvent", 0x200, 0, NULL );
	//...
	UsbMainThreadID = sceKernelCreateThread( "USBMainThread", UsbMainThread, 10, 0x10000, 0, NULL );
	//...
	sceKernelStartThread( UsbMainThreadID, 0, NULL );
	//...
}

Так вот, а вызываемый UsbAttach() устанавливает флаг USB_EVENT_ATTACH во флаговой переменной объекта UsbMainEventFlag:

static int UsbAttach(int speed, void *arg2, void *arg3)
 {
	sceKernelSetEventFlag( UsbMainEventFlag, USB_EVENT_ATTACH);
	return 0;
}

При этом в предварительно созданном при вызове UsbStartFunc() потоке UsbMainThread() написано:

static int UsbMainThread(SceSize size, void *argp)
{
	int ret;
	u32 result;

	while(1)
	{
		ret = sceKernelWaitEventFlag(UsbMainEventFlag, USB_EVENT_ATTACH | USB_EVENT_ASYNC, PSP_EVENT_WAITOR | PSP_EVENT_WAITCLEAR, &result, NULL);
		if(ret < 0)
		{
			sceKernelExitDeleteThread(0); 
		}

		if(result&USB_EVENT_ASYNC)
		{
			usb_async_events++;//nyashkoshkko: debug
			SetUsbAyncReq(&PSPUsbData, sizeof(PSPUsbData));
		}

		if(result&USB_EVENT_ATTACH)
		{
			usb_attach_events++;//nyashkoshkko: debug
			SetUsbAyncReq(&PSPUsbData, sizeof(PSPUsbData));
		}
	}
	return 0;
}

А это значит, что поток в бесконечном цикле ждет установки флага USB_EVENT_ATTACH или флага USB_EVENT_ASYNC во флаговой переменной объекта UsbMainEventFlag. Успешная установка связи с USB-хостом вызвала установку флага USB_EVENT_ATTACH, по которому этот поток выполняет асинхронный запрос на прием пакета данных по USB, при этом сбрасывая флаг USB_EVENT_ASYNC:

static int SetUsbAyncReq( void *data, int size )
{
	//...
	UsbAsyncReq.data = data;
	UsbAsyncReq.size = size;
	UsbAsyncReq.func = UsbAsyncReqDone;
	sceKernelClearEventFlag( UsbMainEventFlag, ~USB_EVENT_ASYNC );
	return( sceUsbbdReqRecv( &UsbAsyncReq ) );
}

В этом запросе callback-ом устанавливается вызов функции UsbAsyncReqDone():

static int UsbAsyncReqDone( struct UsbdDeviceReq *req, int arg2, int arg3 )
{
	sceKernelSetEventFlag( UsbMainEventFlag, USB_EVENT_ASYNC );
	return( 0 );
}

Эта функция, как мы видим, по завершению приема пакета данных от USB-хоста (обрабатываемого ядром операционной системы PSP по прерыванию от USB-контроллера PSP) выставляет флаг USB_EVENT_ASYNC во флаговой переменной объекта UsbMainEventFlag. По нему наш бесконечный цикл выставляет новый асинхронный запрос данных. Такой механизм событий позволяет не тратить процессорное время впустую на бесконечный опрос флага готовности данных, поскольку при вызове sceKernelWaitEventFlag() потоку не выделяются кванты времени до тех пор, пока не наступит необходимое событие — это обеспечивает планировщик потоков внутри операционной системы PSP, да и вообще этот базовый принцип работает в любой многозадачной операционной системе.

Написание сервиса для работы с USB для Linux под Raspberry Pi

Итак, PSP-часть завершена. Теперь время для разработки приложения, а точнее сервиса, который будет запускаться автоматически при включении Raspberry Pi под Linux. Вообще, для Raspberry Pi существует несколько адаптированных дистрибутивов Linux. Но я остановился на Fedora, т.к. он черпает свои корни от Red Hat, с которым я банально имел дело по работе и привык к его RPM дистрибуции пакетов. Сразу после установки Fedora Remix 18 и, в случае необходимости, настройки сети (в моем случае нужно было руками прописать сетевой адрес и шлюз, т.к. DHCP-сервер в домашней сети работает некорректно), которая делается интуитивно — подключив мышь и кликнув на значок сетевого соединения в правом верхнем углу, прямо из коробки работает SSH-сервер. А вот SMB-сервер оперативно настроить не удалось (проблемы с smbpasswd), поэтому исходник создавался и редактировался удаленно по SSH через midnight commander. Первое, с чего я начал, это подключение к PSP. Для этого нужно было узнать, как в Linux взаимодействовать с USB. В связи с этим произошла небольшая неприятная история, из-за которой, в очередной раз, у меня перед глазами рушится весь шарм линукса. Дело в том, что при попытке установить библиотеку и заголовочник для компиляции через

> yum install libusb1-devel

менеджер пакетов поругался и сказал что он устарел, используй libusbx. Хорошо, команда

> yum install libusbx-devel

скачала необходимые файлы. Но дело то в том, что libusbx по api-вызовам несовместим с libusb1, который одинаков с windows-версией libusb, которая, в свою очередь, использовалась в оригинальных исходниках RemoteJoyLite, да и вообще отлично работает под Windows. Но, ладно. С usb разобрались, теперь перейдем к доступу к геймпаду из под линукс. У меня в распоряжении есть проводной геймпад от Xbox 360, который отлично чувствует себя в Windows, и, на удивление, заработал из коробки в Fedora Remix 18 на Raspberry Pi, создав устройство /dev/input/js0. Это отработал штатный драйвер xpad. Существует альтернативный драйвер xboxdrv — он более гибок в конфигурировании. Но нам хватит и штатного.

Кстати, у андроида дела обстоят точно так же.

Драйвер xpad входит в состав ядра Linux для андроида:

#define DRIVER_DESC "X-Box pad driver"

И точно так же создается устройство /dev/input/js0:

MODULE_SUPPORTED_DEVICE("input/js");

Например рассмотрим, как происходит получение списка устройств ввода в андроиде. Рекомендуемое api говорит нам для этого вызвать getDeviceIds(), в котором написано:

    /**
     * Gets the ids of all input devices in the system.
     * @return The input device ids.
     */
    public static int[] getDeviceIds() {
        return InputManager.getInstance().getInputDeviceIds();
    }

getInputDeviceIds():

    private final IInputManager mIm;

    //...

    private SparseArray<InputDevice> mInputDevices;

    //...

    /**
     * Gets the ids of all input devices in the system.
     * @return The input device ids.
     */
    public int[] getInputDeviceIds() {
        synchronized (mInputDevicesLock) {
            populateInputDevicesLocked();

            final int count = mInputDevices.size();
            final int[] ids = new int[count];
            for (int i = 0; i < count; i++) {
                ids[i] = mInputDevices.keyAt(i);
            }
            return ids;
        }
    }

    //...

    private void populateInputDevicesLocked() {
        if (mInputDevicesChangedListener == null) {
            final InputDevicesChangedListener listener = new InputDevicesChangedListener();
            try {
                mIm.registerInputDevicesChangedListener(listener);
            } catch (RemoteException ex) {
                throw new RuntimeException(
                        "Could not get register input device changed listener", ex);
            }
            mInputDevicesChangedListener = listener;
        }

        if (mInputDevices == null) {
            final int[] ids;
            try {
                ids = mIm.getInputDeviceIds();
            } catch (RemoteException ex) {
                throw new RuntimeException("Could not get input device ids.", ex);
            }

            mInputDevices = new SparseArray<InputDevice>();
            for (int i = 0; i < ids.length; i++) {
                mInputDevices.put(ids[i], null);
            }
        }
    }

mIm.getInputDeviceIds():

interface IInputManager {
    // Gets input device information.
    InputDevice getInputDevice(int deviceId);
    int[] getInputDeviceIds();
    //...

Тут вступает в дело сервис InputManager:

import android.view.InputDevice;

//...

/*
 * Wraps the C++ InputManager and provides its callbacks.
 */
public class InputManagerService extends IInputManager.Stub
        implements Watchdog.Monitor, DisplayManagerService.InputManagerFuncs {
    static final String TAG = "InputManager";
    static final boolean DEBUG = false;

    private static final String EXCLUDED_DEVICES_PATH = "etc/excluded-input-devices.xml";

    //...

    private InputDevice[] mInputDevices = new InputDevice[0];

    //...

    /**
     * Gets the ids of all input devices in the system.
     * @return The input device ids.
     */
    @Override // Binder call
    public int[] getInputDeviceIds() {
        synchronized (mInputDevicesLock) {
            final int count = mInputDevices.length;
            int[] ids = new int[count];
            for (int i = 0; i < count; i++) {
                ids[i] = mInputDevices[i].getId();
            }
            return ids;
        }
    }

getId() приводит нас уже туда, где мы были (андроид удивителен):

public final class InputDevice implements Parcelable {
    private final int mId;

    //...

    /**
     * Gets the input device id.
     * <p>
     * Each input device receives a unique id when it is first configured
     * by the system.  The input device id may change when the system is restarted or if the
     * input device is disconnected, reconnected or reconfigured at any time.
     * If you require a stable identifier for a device that persists across
     * boots and reconfigurations, use {@link #getDescriptor()}.
     * </p>
     *
     * @return The input device id.
     */
    public int getId() {
        return mId;
    }

    //...

    // Called by native code.
    private InputDevice(int id, int generation, int controllerNumber, String name, int vendorId,
            int productId, String descriptor, boolean isExternal, int sources, int keyboardType,
            KeyCharacterMap keyCharacterMap, boolean hasVibrator, boolean hasButtonUnderPad) {
        mId = id;
        mGeneration = generation;
        mControllerNumber = controllerNumber;
        mName = name;
        mVendorId = vendorId;
        mProductId = productId;
        mDescriptor = descriptor;
        mIsExternal = isExternal;
        mSources = sources;
        mKeyboardType = keyboardType;
        mKeyCharacterMap = keyCharacterMap;
        mHasVibrator = hasVibrator;
        mHasButtonUnderPad = hasButtonUnderPad;
    }

    private InputDevice(Parcel in) {
        mId = in.readInt();
        mGeneration = in.readInt();
        mControllerNumber = in.readInt();
        mName = in.readString();
        mVendorId = in.readInt();
        mProductId = in.readInt();
        mDescriptor = in.readString();
        mIsExternal = in.readInt() != 0;
        mSources = in.readInt();
        mKeyboardType = in.readInt();
        mKeyCharacterMap = KeyCharacterMap.CREATOR.createFromParcel(in);
        mHasVibrator = in.readInt() != 0;
        mHasButtonUnderPad = in.readInt() != 0;

        for (;;) {
            int axis = in.readInt();
            if (axis < 0) {
                break;
            }
            addMotionRange(axis, in.readInt(), in.readFloat(), in.readFloat(), in.readFloat(),
                    in.readFloat(), in.readFloat());
        }
    }

В нативной части андроида экземпляр InputDevice() создается здесь (спасибо оперативному ответу с тостера):

jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& deviceInfo) {

    //...

    ScopedLocalRef<jobject> inputDeviceObj(env, env->NewObject(gInputDeviceClassInfo.clazz,
                gInputDeviceClassInfo.ctor, deviceInfo.getId(), deviceInfo.getGeneration(),
                deviceInfo.getControllerNumber(), nameObj.get(),
                static_cast<int32_t>(ident.vendor), static_cast<int32_t>(ident.product),
                descriptorObj.get(), deviceInfo.isExternal(), deviceInfo.getSources(),
                deviceInfo.getKeyboardType(), kcmObj.get(), deviceInfo.hasVibrator(),
                deviceInfo.hasButtonUnderPad()));

Эта функция вызывается здесь:

void NativeInputManager::notifyInputDevicesChanged(const Vector<InputDeviceInfo>& inputDevices) {
    JNIEnv* env = jniEnv();

    //...
            jobject inputDeviceObj = android_view_InputDevice_create(env, inputDevices.itemAt(i));

Вызов функции notifyInputDevicesChanged() определен колбэком опять в уже знакомом сервисе InputManager:

    // Native callback.
    private void notifyInputDevicesChanged(InputDevice[] inputDevices) {
        synchronized (mInputDevicesLock) {
            if (!mInputDevicesChangedPending) {
                mInputDevicesChangedPending = true;
                mHandler.obtainMessage(MSG_DELIVER_INPUT_DEVICES_CHANGED,
                        mInputDevices).sendToTarget();
            }

            mInputDevices = inputDevices;
        }
    }

Вызов самого колбэка инициируется в InputReader:

void InputReader::loopOnce() {

    //...

    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

    //...

    // Send out a message that the describes the changed input devices.
    if (inputDevicesChanged) {
        mPolicy->notifyInputDevicesChanged(inputDevices);
    }

Так же здесь мы видим, что события от устройства ввода принимаются классом EventHubInterface:

    sp<EventHubInterface> mEventHub;

И, в итоге, в реализации этого класса EventHub.cpp идет открытие и работа с устройством из /dev/input, как и в обычном Linux Fedora в Raspberry Pi:

    static const char *DEVICE_PATH = "/dev/input";

    //...

    char devname[PATH_MAX];
    char *filename;

    //...

    strcpy(devname, DEVICE_PATH);
    filename = devname + strlen(devname);
    *filename++ = '/';

    //...

    strcpy(filename, event->name);

    //...

    openDeviceLocked(devname);

    //...

status_t EventHub::openDeviceLocked(const char *devicePath) {
    char buffer[80];

    ALOGV("Opening device: %s", devicePath);

    int fd = open(devicePath, O_RDWR | O_CLOEXEC);
    if(fd < 0) {
        ALOGE("could not open %s, %sn", devicePath, strerror(errno));
        return -1;
    }

А вообще, про всю эту систему ввода в андроиде вкратце объяснено в самих исходниках здесь:

/*
 * The input manager is the core of the system event processing.
 *
 * The input manager uses two threads.
 *
 * 1. The InputReaderThread (called "InputReader") reads and preprocesses raw input events,
 *    applies policy, and posts messages to a queue managed by the DispatcherThread.
 * 2. The InputDispatcherThread (called "InputDispatcher") thread waits for new events on the
 *    queue and asynchronously dispatches them to applications.
 *
 * By design, the InputReaderThread class and InputDispatcherThread class do not share any
 * internal state.  Moreover, all communication is done one way from the InputReaderThread
 * into the InputDispatcherThread and never the reverse.  Both classes may interact with the
 * InputDispatchPolicy, however.
 *
 * The InputManager class never makes any calls into Java itself.  Instead, the
 * InputDispatchPolicy is responsible for performing all external interactions with the
 * system, including calling DVM services.
 */
class InputManagerInterface : public virtual RefBase {

Итак, итоговый исходный код сервиса имеет следующий вид:

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <libusb-1.0/libusb.h>

#define SONY_VENDOR_ID	 0x054C
#define PSP_B_PRODUCT_ID 0x01C9

#define UP			0x00000010
#define DOWN			0x00000040
#define LEFT			0x00000080
#define RIGHT			0x00000020
#define B_X			0x00004000
#define B_O			0x00002000
#define B_KVADRAT	0x00008000
#define B_TREUGOLNIK	0x00001000
#define B_L			0x00000100
#define B_R			0x00000200
#define B_SELECT	0x00000001
#define B_START			0x00000008
#define B_NOTE			0x00800000

struct
{
    unsigned int Pattern;
    unsigned int Btn;
    unsigned int X;
    unsigned int Y;
} PS = {0x1234ABFE, 0, 127, 127};

struct js_event
{
    unsigned int time;
    short value;
    unsigned char type;
    unsigned char number;
};

int is_usbdevblock(libusb_device *dev)
{
	struct libusb_device_descriptor desc;
	int r = libusb_get_device_descriptor(dev, &desc);
	if((desc.idVendor == SONY_VENDOR_ID) && (desc.idProduct == PSP_B_PRODUCT_ID))
	{
		return 1;
	}

	return 0;
}

int main(int argc, char** argv)
{
	unsigned int real_x = 0, real_y = 0;
	int x, y;

	int fd = 0;

	while(1)
	{
		libusb_device **list;
		libusb_device *found = NULL;
		libusb_context *ctx = NULL;
		int attached = 0;

		libusb_init(&ctx);
		libusb_set_debug(ctx, 3); 
	
		ssize_t cnt = libusb_get_device_list(ctx, &list);
		ssize_t i = 0;
		int err = 0;
		if(cnt < 0)
		{
			return -1;
		}

		for(i = 0; i < cnt; i++)
		{
			libusb_device *device = list[i];
			if(is_usbdevblock(device))
			{
				found = device;
				break;
			}
		}

		if(found)
		{
			libusb_device_handle *handle;	
			err = libusb_open(found, &handle);
			if (err)
			{
				return -1;
			}

			if (libusb_kernel_driver_active(handle, 0))
			{ 
				libusb_detach_kernel_driver(handle, 0); 
				attached = 1;
			}

			err = libusb_claim_interface(handle, 0);
			if (err)
			{
				return -1;
			}
			
			if(fd == 0)
			{
				fd = open("/dev/input/js0", O_RDONLY);
			}

			if(fd < 0)
			{
				goto clean;
			}
			
			int nEndpoint = 0x01;
			int nTimeout = 500; //in milliseconds
			int BytesWritten = 0;
			int ret;

			struct js_event e;
			int t;

			while(1)
			{
				read(fd, &e, sizeof(struct js_event));
				
				e.type &= ~0x80;
				
				t = 0; //transfer = 0;
				
				if(e.type == 1)
				{
					if(e.value == 1)
					{
						if(e.number == 0) {PS.Btn |= B_X; t = 1;}
						if(e.number == 1) {PS.Btn |= B_O; t = 1;}
						if(e.number == 2) {PS.Btn |= B_KVADRAT; t = 1;}
						if(e.number == 3) {PS.Btn |= B_TREUGOLNIK; t = 1;}
						if(e.number == 4) {PS.Btn |= B_L; t = 1;}
						if(e.number == 5) {PS.Btn |= B_R; t = 1;}
						if(e.number == 6) {PS.Btn |= B_SELECT; t = 1;}
						if(e.number == 7) {PS.Btn |= B_START; t = 1;}
						if(e.number == 8) {PS.Btn |= B_NOTE; t = 1;}//XBOX_HOME
						//if(e.number == 9) PS.Btn |= ;//L_STICK_PRESS
						//if(e.number == 10)PS.Btn |= ;//R_STICK_PRESS
					}
					
					if(e.value == 0)
					{
						if(e.number == 0) {PS.Btn &= ~B_X; t = 1;}
						if(e.number == 1) {PS.Btn &= ~B_O; t = 1;}
						if(e.number == 2) {PS.Btn &= ~B_KVADRAT; t = 1;}
						if(e.number == 3) {PS.Btn &= ~B_TREUGOLNIK; t = 1;}
						if(e.number == 4) {PS.Btn &= ~B_L; t = 1;}
						if(e.number == 5) {PS.Btn &= ~B_R; t = 1;}
						if(e.number == 6) {PS.Btn &= ~B_SELECT; t = 1;}
						if(e.number == 7) {PS.Btn &= ~B_START; t = 1;}
						if(e.number == 8) {PS.Btn &= ~B_NOTE; t = 1;}
					}
				}
				
				if(e.type == 2)
				{
					if(e.number == 6)
					{
						if(e.value == -32767) {PS.Btn |= LEFT; t = 1;}
						if(e.value ==  32767) {PS.Btn |= RIGHT; t = 1;}
						if(e.value ==	0) {PS.Btn &= ~(LEFT | RIGHT); t = 1;}
    				}
    				if(e.number == 7)
					{
						if(e.value == -32767) {PS.Btn |= UP; t = 1;}
						if(e.value ==  32767) {PS.Btn |= DOWN; t = 1;}
						if(e.value ==	0) {PS.Btn &= ~(UP | DOWN); t = 1;}
    				}
    				if(e.number == 0)
    				{
    					if(real_x != ((e.value + 32767) / 256)) {real_x = ((e.value + 32767) / 256); t = 1;}
					}
    				if(e.number == 1)
    				{
    					if(real_y != ((e.value + 32767) / 256)) {real_y = ((e.value + 32767) / 256); t = 1;}
					}
				}
				
				if(t == 1)
				{
					#define KOEF 1.4
					//[-128..0..127]
					x = real_x - 128;
					y = real_y - 128;
					x = x * (1. + ((abs(x) * (KOEF-1.))/(127./KOEF))); if(x > 127) x = 127; if(x < -128) x = -128;
					y = y * (1. + ((abs(y) * (KOEF-1.))/(127./KOEF))); if(y > 127) y = 127; if(y < -128) y = -128;
					PS.X = 128 + x;
					PS.Y = 128 + y;
					
					ret = libusb_bulk_transfer(handle, nEndpoint, (unsigned char *)&PS, sizeof(PS), &BytesWritten, nTimeout);
					if(ret < 0) 
					{
						break;
					}
				}
			}
			
			clean:	
			
			if(fd)
			{
				close(fd);
				fd = 0;
			}

			if(attached == 1)
			{
				libusb_attach_kernel_driver(handle, 0);
			}
			
			libusb_close(handle);
		}

		libusb_free_device_list(list, 1);
		libusb_exit(ctx);
		
		sleep(1);
	}

	return 0;
}

Вот ссылка на двоичный файл сервиса. А вот также содержимое скрипта компиляции m.sh:

gcc xbox2psp.c -o xbox2psp.o -I/usr/local -L/usr/local -lusb-1.0

По самому исходнику хотелось бы отметить два момента. Во-первых, аналоговые стики Xbox имеют точность 16 бит, в то время как стик PSP имеет точность 8 бит. В связи с этим пакет я шлю по изменению приведенного к 8 битам значения осей, а не по изменению исходных данных от контроллера Xbox. Во-вторых, в PSP значение диагоналей соответствует полной шкале (т.е. круглый стик PSP с точки зрения шкалы — это квадрат), а в Xbox, как и положено, половине шкалы:
Подключаем к PSP геймпад от Xbox 360 при помощи Raspberry Pi

Поэтому введен линейно увеличивающийся (чем больше отклонение от центра оси контроллера Xbox, тем больше) коэффициент с максимумом в 1.4 (хотя, как выяснилось позже, правильно было бы определять значение угла, и чем ближе угол к диагонали, тем больше делать коэффициент). С этими значениями геймпад Xbox ощущается без какого то ни было дискомфорта, хоть чисто технически чувствительность и получилась загрубленной. В Doom 0.05 управлять удобно, в Dungeon Siege все три скорости перемещения (в зависимости от силы отклонения стика) работают и ощущаются как на самой PSP. Ибо, при столкновении с проблемой, в начале был опробован простой коэффициент (и 1.5, и 1.4), без линейного увеличения в зависимости от отклонения, и в названных играх ощущался резкий дискомфорт — играть было невозможно.

Добавление собственного сервиса в автозагрузку в Fedora Remix 18 для Raspberry Pi

Поверхностное гугление по вопросу добавления программы в автозагрузку в Linux дает в основном рекомендации по модификации скрипта init rc. Но в нашем случае нужно поступить по-другому.

1. Сначала нужно скопировать наш сервис xbox2psp.o в /usr/local/bin и установить ему права запуска(все три бита).

2. Затем создать файл /lib/systemd/system/xbox2psp.service следующего содержания:

[Unit]
Description=xbox2psp
After=syslog.target network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/xbox2psp.o

[Install]
WantedBy=multi-user.target

3. Перейти в папку /etc/systemd/system/ и создать ссылку командой

> ln -s /lib/systemd/system/xbox2psp.service xbox2psp.service

4. Перезагрузить конфигурацию демона автозагрузки:

> systemctl daemon-reload

5. Активировать автозапуск нового сервиса

> systemctl enable xbox2psp.service

6. При необходимости можно сразу запустить сервис командой

> systemctl start xbox2psp.service

В итоге мы получили удобную возможность управлять PSP при помощи геймпада от Xbox 360. При желании этот проект можно модифицировать для подключения например Dualshock 3 по Bluetooth.

Автор: Nyashkoshkko

Источник


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


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