From 7bacbdf10b97040c9ed282a22485759bd082fa63 Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Sun, 19 Nov 2017 21:18:26 +0100 Subject: Add virtual joystick output for macOS Based of the foohid IOKit driver for implementing virtual HID devices. Issue: #236 --- proto-iokit-foohid/CMakeLists.txt | 4 + proto-iokit-foohid/README.md | 17 +++ proto-iokit-foohid/foohidjoystick.cpp | 162 +++++++++++++++++++++++++++++ proto-iokit-foohid/foohidjoystick.h | 51 +++++++++ proto-iokit-foohid/iokitprotocol.cpp | 51 +++++++++ proto-iokit-foohid/iokitprotocol.h | 34 ++++++ proto-iokit-foohid/iokitprotocoldialog.cpp | 30 ++++++ proto-iokit-foohid/iokitprotocoldialog.h | 21 ++++ proto-iokit-foohid/lang/nl_NL.ts | 40 +++++++ proto-iokit-foohid/lang/ru_RU.ts | 40 +++++++ proto-iokit-foohid/lang/stub.ts | 40 +++++++ 11 files changed, 490 insertions(+) create mode 100644 proto-iokit-foohid/CMakeLists.txt create mode 100644 proto-iokit-foohid/README.md create mode 100644 proto-iokit-foohid/foohidjoystick.cpp create mode 100644 proto-iokit-foohid/foohidjoystick.h create mode 100644 proto-iokit-foohid/iokitprotocol.cpp create mode 100644 proto-iokit-foohid/iokitprotocol.h create mode 100644 proto-iokit-foohid/iokitprotocoldialog.cpp create mode 100644 proto-iokit-foohid/iokitprotocoldialog.h create mode 100644 proto-iokit-foohid/lang/nl_NL.ts create mode 100644 proto-iokit-foohid/lang/ru_RU.ts create mode 100644 proto-iokit-foohid/lang/stub.ts diff --git a/proto-iokit-foohid/CMakeLists.txt b/proto-iokit-foohid/CMakeLists.txt new file mode 100644 index 00000000..b997bf60 --- /dev/null +++ b/proto-iokit-foohid/CMakeLists.txt @@ -0,0 +1,4 @@ +IF(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + otr_module(proto-iokit-foohid) + otr_prop(TARGET opentrack-proto-iokit-foohid LINK_FLAGS "-framework IOKit") +endif() diff --git a/proto-iokit-foohid/README.md b/proto-iokit-foohid/README.md new file mode 100644 index 00000000..a9d7a34e --- /dev/null +++ b/proto-iokit-foohid/README.md @@ -0,0 +1,17 @@ +# Virtual Joystick Support for macOS + +Adds support for a virtual joystick output, based on the foohid driver for virtual HID devices. + +## Usage + +Install the latest [foohid HID driver](https://github.com/unbit/foohid/releases/). + +Select "Virtual Joystick" as the output in OpenTrack. + +The X, Y, Z, Yaw, Pitch, and Roll tracking axes are mapped to the joystick's X, Y, Z, RX, RY, +and RZ axes respectively. + +# Building + +The implementation only uses standard libraries that are pre-installed on macOS. There are no +special requirements for building. diff --git a/proto-iokit-foohid/foohidjoystick.cpp b/proto-iokit-foohid/foohidjoystick.cpp new file mode 100644 index 00000000..17a4bcb5 --- /dev/null +++ b/proto-iokit-foohid/foohidjoystick.cpp @@ -0,0 +1,162 @@ +/* Copyright (c) 2017, Eike Ziller * + * * + * Permission to use, copy, modify, and/or distribute this software for any * + * purpose with or without fee is hereby granted, provided that the above * + * copyright notice and this permission notice appear in all copies. * + */ + +#include "foohidjoystick.h" + +#include + +const char FOOHID_SERVICE_NAME[] = "it_unbit_foohid"; + +enum class FooHIDMethod { + Create = 0, + Destroy, + Send, + List, + Subscribe +}; + +// Joystick USB descriptor +static unsigned char report_descriptor[] = { + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x04, // USAGE (Joystick) + 0xa1, 0x01, // COLLECTION (Application) + 0x09, 0x01, // USAGE (Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x32, // USAGE (Z) + 0x09, 0x33, // USAGE (RX) + 0x09, 0x34, // USAGE (RY) + 0x09, 0x35, // USAGE (RZ) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x06, // REPORT_COUNT (6) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0xc0, // END_COLLECTION + 0xc0 // END_COLLECTION +}; + +static bool connectToService(io_connect_t *connection, QString *errorMessage) +{ + io_iterator_t iterator; + io_service_t service; + // Get an iterator over all IOService instances + kern_return_t ret = IOServiceGetMatchingServices(kIOMasterPortDefault, + IOServiceMatching(FOOHID_SERVICE_NAME), + &iterator); + if (ret != KERN_SUCCESS) { + *errorMessage = QCoreApplication::translate("iokit-foohid", "Unable to find FooHID IOService."); + return false; + } + // Iterate over services and try to open connection + bool found = false; + while ((service = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { + ret = IOServiceOpen(service, mach_task_self(), 0, connection); + if (ret == KERN_SUCCESS) { + found = true; + break; + } + IOObjectRelease(service); + } + IOObjectRelease(iterator); + if (!found) { + *errorMessage = QCoreApplication::translate("iokit-foohid", "Unable to connect to FooHID IOService."); + return false; + } + return true; +} + +static void disconnectFromService(io_connect_t connection) +{ + IOServiceClose(connection); + // ignore errors +} + +FooHIDJoystick::FooHIDJoystick(const QByteArray &name, const QByteArray &serialNumber) + : name(name), serialNumber(serialNumber) +{ + connectionOpened = connectToService(&connection, &_errorMessage); + _hasError = !connectionOpened; + if (!_hasError) { + // first try to destroy device, in case it was left-over (from a crash/interrupt) + destroyDevice(); + deviceCreated = createDevice(); + _hasError = !deviceCreated; + if (!deviceCreated) + _errorMessage = QCoreApplication::translate("iokit-foohid", + "Failed to create virtual joystick"); + } +} + +FooHIDJoystick::~FooHIDJoystick() +{ + if (deviceCreated) + destroyDevice(); + if (connectionOpened) + disconnectFromService(connection); +} + +bool FooHIDJoystick::hasError() const +{ + return _hasError; +} + +QString FooHIDJoystick::errorMessage() const +{ + return _errorMessage; +} + +void FooHIDJoystick::setValue(JoystickValues newValues) +{ + values = newValues; + if (!sendToDevice()) { + _hasError = true; + _errorMessage = QCoreApplication::translate("iokit-foohid", + "Failed to send values to virtual joystick"); + } +} + +bool FooHIDJoystick::createDevice() const +{ + uint64_t params[8]; + params[0] = uint64_t(name.constData()); // pointer to name + params[1] = uint64_t(name.size()); // size of name without \0 + params[2] = uint64_t(report_descriptor); // pointer to report descriptor + params[3] = sizeof(report_descriptor); // size of report descriptor + params[4] = uint64_t(serialNumber.constData()); // pointer to serial number + params[5] = uint64_t(serialNumber.size()); // size of serial number without \0 + params[6] = uint64_t(2); // vendor ID + params[7] = uint64_t(3); // device ID + + kern_return_t ret = IOConnectCallScalarMethod(connection, int(FooHIDMethod::Create), params, + sizeof(params)/sizeof(params[0]), NULL, 0); + return ret == KERN_SUCCESS; +} + +bool FooHIDJoystick::sendToDevice() const +{ + uint64_t params[4]; + params[0] = uint64_t(name.constData()); // pointer to name + params[1] = uint64_t(name.size()); // size of name without \0 + params[2] = uint64_t(&values); // pointer to values + params[3] = sizeof(struct JoystickValues); // length of value struct + kern_return_t ret = IOConnectCallScalarMethod(connection, int(FooHIDMethod::Send), params, + sizeof(params)/sizeof(params[0]), NULL, 0); + return ret == KERN_SUCCESS; +} + +void FooHIDJoystick::destroyDevice() const +{ + uint64_t params[2]; + params[0] = uint64_t(name.constData()); // pointer to name + params[1] = uint64_t(name.size()); // size of name without \0 + IOConnectCallScalarMethod(connection, int(FooHIDMethod::Destroy), params, + sizeof(params)/sizeof(params[0]), NULL, 0); + // ignore failure +} diff --git a/proto-iokit-foohid/foohidjoystick.h b/proto-iokit-foohid/foohidjoystick.h new file mode 100644 index 00000000..a1f74304 --- /dev/null +++ b/proto-iokit-foohid/foohidjoystick.h @@ -0,0 +1,51 @@ +/* Copyright (c) 2017, Eike Ziller * + * * + * Permission to use, copy, modify, and/or distribute this software for any * + * purpose with or without fee is hereby granted, provided that the above * + * copyright notice and this permission notice appear in all copies. * + */ + +#pragma once + +#include +#include + +#include + +struct JoystickValues { + uint8_t x; + uint8_t y; + uint8_t z; + uint8_t rx; + uint8_t ry; + uint8_t rz; +}; + +class FooHIDJoystick +{ +public: + FooHIDJoystick(const QByteArray &name, const QByteArray &serialNumber); + ~FooHIDJoystick(); + + bool hasError() const; + QString errorMessage() const; + + int minValue() const { return 0; } + int range() const { return 255; } + + void setValue(JoystickValues newValues); + +private: + bool createDevice() const; + bool sendToDevice() const; + void destroyDevice() const; + + const QByteArray name; + const QByteArray serialNumber; + io_connect_t connection = 0; + JoystickValues values = {127, 127}; + QString _errorMessage; + bool _hasError = true; + bool connectionOpened = false; + bool deviceCreated = false; +}; diff --git a/proto-iokit-foohid/iokitprotocol.cpp b/proto-iokit-foohid/iokitprotocol.cpp new file mode 100644 index 00000000..4af6ad5b --- /dev/null +++ b/proto-iokit-foohid/iokitprotocol.cpp @@ -0,0 +1,51 @@ +/* Copyright (c) 2017, Eike Ziller * + * * + * Permission to use, copy, modify, and/or distribute this software for any * + * purpose with or without fee is hereby granted, provided that the above * + * copyright notice and this permission notice appear in all copies. * + */ + +#include "iokitprotocol.h" + +#include "foohidjoystick.h" +#include "iokitprotocoldialog.h" + +#include + +IOKitProtocol::IOKitProtocol() +{ + joystick = std::make_unique("OpenTrack Virtual Joystick", + "SN 983457"); + if (joystick->hasError()) + qWarning("%s\n", qPrintable(joystick->errorMessage())); +} + +bool IOKitProtocol::correct() +{ + return joystick && !joystick->hasError(); +} + +static uint8_t valueToStick(FooHIDJoystick *stick, double min, double value, double max) +{ + return uint8_t(qBound(stick->minValue(), + int(round((value - min) * stick->range() / (max - min) - stick->minValue())), + stick->minValue() + stick->range())); +} + +void IOKitProtocol::pose(const double *headpose) +{ + const uint8_t x = valueToStick(joystick.get(), -75., headpose[0], +75.); + const uint8_t y = valueToStick(joystick.get(), -75., headpose[1], +75.); + const uint8_t z = valueToStick(joystick.get(), -75., headpose[2], +75.); + const uint8_t rx = valueToStick(joystick.get(), -180., headpose[3], +180.); + const uint8_t ry = valueToStick(joystick.get(), -180., headpose[4], +180.); + const uint8_t rz = valueToStick(joystick.get(), -180., headpose[5], +180.); + joystick->setValue({x, y, z, rx, ry, rz}); +} + +QString IOKitProtocol::game_name() +{ + return QString(); +} + +OPENTRACK_DECLARE_PROTOCOL(IOKitProtocol, IOKitProtocolDialog, IOKitProtocolMetadata) diff --git a/proto-iokit-foohid/iokitprotocol.h b/proto-iokit-foohid/iokitprotocol.h new file mode 100644 index 00000000..f3f4089f --- /dev/null +++ b/proto-iokit-foohid/iokitprotocol.h @@ -0,0 +1,34 @@ +/* Copyright (c) 2017, Eike Ziller * + * * + * Permission to use, copy, modify, and/or distribute this software for any * + * purpose with or without fee is hereby granted, provided that the above * + * copyright notice and this permission notice appear in all copies. * + */ + +#pragma once + +#include "api/plugin-api.hpp" + +#include + +class FooHIDJoystick; + +class IOKitProtocol : public IProtocol +{ +public: + IOKitProtocol(); + + bool correct() final; + void pose(const double *headpose) final; + QString game_name() final; + +private: + std::unique_ptr joystick; +}; + +class IOKitProtocolMetadata : public Metadata +{ +public: + QString name() { return QString(QCoreApplication::translate("iokit-foohid", "Virtual joystick")); } + QIcon icon() { return QIcon(":/images/facetracknoir.png"); } +}; diff --git a/proto-iokit-foohid/iokitprotocoldialog.cpp b/proto-iokit-foohid/iokitprotocoldialog.cpp new file mode 100644 index 00000000..443a30b6 --- /dev/null +++ b/proto-iokit-foohid/iokitprotocoldialog.cpp @@ -0,0 +1,30 @@ +/* Copyright (c) 2017, Eike Ziller * + * * + * Permission to use, copy, modify, and/or distribute this software for any * + * purpose with or without fee is hereby granted, provided that the above * + * copyright notice and this permission notice appear in all copies. * + */ + +#include "iokitprotocoldialog.h" + +#include +#include +#include + +IOKitProtocolDialog::IOKitProtocolDialog() +{ + setLayout(new QVBoxLayout); + layout()->addWidget(new QLabel(tr("No settings available."))); + layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Ignored, QSizePolicy::Expanding)); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); + layout()->addWidget(buttonBox); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); +} + +void IOKitProtocolDialog::register_protocol(IProtocol *) +{ +} + +void IOKitProtocolDialog::unregister_protocol() +{ +} diff --git a/proto-iokit-foohid/iokitprotocoldialog.h b/proto-iokit-foohid/iokitprotocoldialog.h new file mode 100644 index 00000000..0417ba26 --- /dev/null +++ b/proto-iokit-foohid/iokitprotocoldialog.h @@ -0,0 +1,21 @@ +/* Copyright (c) 2017, Eike Ziller * + * * + * Permission to use, copy, modify, and/or distribute this software for any * + * purpose with or without fee is hereby granted, provided that the above * + * copyright notice and this permission notice appear in all copies. * + */ + +#pragma once + +#include "api/plugin-api.hpp" + +class IOKitProtocolDialog : public IProtocolDialog +{ + Q_OBJECT + +public: + IOKitProtocolDialog(); + + void register_protocol(IProtocol *protocol) final; + void unregister_protocol() final; +}; diff --git a/proto-iokit-foohid/lang/nl_NL.ts b/proto-iokit-foohid/lang/nl_NL.ts new file mode 100644 index 00000000..f5861f6e --- /dev/null +++ b/proto-iokit-foohid/lang/nl_NL.ts @@ -0,0 +1,40 @@ + + + + + IOKitProtocolDialog + + + No settings available. + + + + + iokit-foohid + + + Virtual joystick + + + + + Unable to find FooHID IOService. + + + + + Unable to connect to FooHID IOService. + + + + + Failed to create virtual joystick + + + + + Failed to send values to virtual joystick + + + + diff --git a/proto-iokit-foohid/lang/ru_RU.ts b/proto-iokit-foohid/lang/ru_RU.ts new file mode 100644 index 00000000..15ab759f --- /dev/null +++ b/proto-iokit-foohid/lang/ru_RU.ts @@ -0,0 +1,40 @@ + + + + + IOKitProtocolDialog + + + No settings available. + + + + + iokit-foohid + + + Virtual joystick + + + + + Unable to find FooHID IOService. + + + + + Unable to connect to FooHID IOService. + + + + + Failed to create virtual joystick + + + + + Failed to send values to virtual joystick + + + + diff --git a/proto-iokit-foohid/lang/stub.ts b/proto-iokit-foohid/lang/stub.ts new file mode 100644 index 00000000..889ade33 --- /dev/null +++ b/proto-iokit-foohid/lang/stub.ts @@ -0,0 +1,40 @@ + + + + + IOKitProtocolDialog + + + No settings available. + + + + + iokit-foohid + + + Virtual joystick + + + + + Unable to find FooHID IOService. + + + + + Unable to connect to FooHID IOService. + + + + + Failed to create virtual joystick + + + + + Failed to send values to virtual joystick + + + + -- cgit v1.2.3