diff options
-rw-r--r-- | tracker-tobii-eyex/CMakeLists.txt | 21 | ||||
-rw-r--r-- | tracker-tobii-eyex/images/tobii-eyex-logo.png | bin | 0 -> 1319 bytes | |||
-rw-r--r-- | tracker-tobii-eyex/tobii-eyex-dialog.cpp | 26 | ||||
-rw-r--r-- | tracker-tobii-eyex/tobii-eyex-dialog.ui | 623 | ||||
-rw-r--r-- | tracker-tobii-eyex/tobii-eyex-res.qrc | 5 | ||||
-rw-r--r-- | tracker-tobii-eyex/tobii-eyex.cpp | 445 | ||||
-rw-r--r-- | tracker-tobii-eyex/tobii-eyex.hpp | 135 |
7 files changed, 1255 insertions, 0 deletions
diff --git a/tracker-tobii-eyex/CMakeLists.txt b/tracker-tobii-eyex/CMakeLists.txt new file mode 100644 index 00000000..a5fb3404 --- /dev/null +++ b/tracker-tobii-eyex/CMakeLists.txt @@ -0,0 +1,21 @@ +if(WIN32) + set(SDK_TOBII_EYEX "" CACHE PATH "") + if(SDK_TOBII_EYEX) + opentrack_boilerplate(opentrack-tracker-tobii-eyex) + set(tobii-libdir ${SDK_TOBII_EYEX}/lib/x86/) + set(tobii-dll ${tobii-libdir}/Tobii.EyeX.Client.dll) + # we only care about the .lib for MSVC++ build anyway + set(tobii-link ${tobii-libdir}/Tobii.EyeX.Client.lib) + target_include_directories(opentrack-tracker-tobii-eyex PRIVATE ${CMAKE_SOURCE_DIR}/spline-widget) + target_link_libraries(opentrack-tracker-tobii-eyex ${tobii-link} opentrack-spline-widget) + # we only ever use the C headers due to Microsoft CRT ABI incompatibility with GNU + set(tobii-incdir ${SDK_TOBII_EYEX}/include/eyex) + target_include_directories(opentrack-tracker-tobii-eyex SYSTEM PUBLIC ${tobii-incdir}) + install(FILES ${tobii-dll} DESTINATION ${opentrack-hier-pfx} ${opentrack-perms}) + if((CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") AND (CMAKE_SYSTEM_NAME STREQUAL "Windows")) + # let's assume 32-bit Windows host systems doesn't exist at all + install(FILES c:/windows/syswow64/msvcp110.dll DESTINATION ${opentrack-hier-pfx} ${opentrack-perms}) + install(FILES c:/windows/syswow64/msvcr110.dll DESTINATION ${opentrack-hier-pfx} ${opentrack-perms}) + endif() + endif() +endif() diff --git a/tracker-tobii-eyex/images/tobii-eyex-logo.png b/tracker-tobii-eyex/images/tobii-eyex-logo.png Binary files differnew file mode 100644 index 00000000..b952891b --- /dev/null +++ b/tracker-tobii-eyex/images/tobii-eyex-logo.png diff --git a/tracker-tobii-eyex/tobii-eyex-dialog.cpp b/tracker-tobii-eyex/tobii-eyex-dialog.cpp new file mode 100644 index 00000000..bb9ffcea --- /dev/null +++ b/tracker-tobii-eyex/tobii-eyex-dialog.cpp @@ -0,0 +1,26 @@ +#include "tobii-eyex.hpp" + +tobii_eyex_dialog::tobii_eyex_dialog() +{ + ui.setupUi(this); + + connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &tobii_eyex_dialog::do_ok); + connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &tobii_eyex_dialog::do_cancel); + + ui.tracking_mode->addItem("Relative", tobii_relative); + ui.tracking_mode->addItem("Absolute", tobii_absolute); + + tie_setting(s.mode, ui.tracking_mode); +} + +void tobii_eyex_dialog::do_ok() +{ + s.b->save(); + rs.b->save(); + close(); +} + +void tobii_eyex_dialog::do_cancel() +{ + close(); +} diff --git a/tracker-tobii-eyex/tobii-eyex-dialog.ui b/tracker-tobii-eyex/tobii-eyex-dialog.ui new file mode 100644 index 00000000..81fc13d6 --- /dev/null +++ b/tracker-tobii-eyex/tobii-eyex-dialog.ui @@ -0,0 +1,623 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>tobii_eyex_dialog_widgets</class> + <widget class="QWidget" name="tobii_eyex_dialog_widgets"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>715</width> + <height>440</height> + </rect> + </property> + <property name="windowTitle"> + <string>Tracker options</string> + </property> + <property name="windowIcon"> + <iconset resource="tobii-eyex-res.qrc"> + <normaloff>:/images/tobii-eyex-logo.png</normaloff>:/images/tobii-eyex-logo.png</iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="bottomMargin"> + <number>4</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Tracking settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Relative mode shifts the view toward a target that may be offscreen then fixes upon it.The absolute mode is not gradual.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QFrame" name="frame"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>4</number> + </property> + <property name="topMargin"> + <number>4</number> + </property> + <property name="rightMargin"> + <number>4</number> + </property> + <property name="bottomMargin"> + <number>4</number> + </property> + <property name="horizontalSpacing"> + <number>9</number> + </property> + <property name="verticalSpacing"> + <number>4</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Tracking mode</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="tracking_mode"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>510</width> + <height>17</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Relative tracking mode gain</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Adjust the gain mapping and speed to suit your preference, game type, display size, and distance from the screen.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QFrame" name="frame_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QFrame" name="frame_3"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="leftMargin"> + <number>4</number> + </property> + <property name="topMargin"> + <number>4</number> + </property> + <property name="bottomMargin"> + <number>4</number> + </property> + <property name="verticalSpacing"> + <number>3</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label_11"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Speed</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="speed_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QSlider" name="speed"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Deadzone</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="deadzone_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QSlider" name="deadzone"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_10"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Exponent</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="exponent_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QSlider" name="exponent"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_5"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Exponential length</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="exponent_len_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QSlider" name="exponent_len"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_7"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Linear coefficient</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLabel" name="linear_c_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QSlider" name="linear_c"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_6"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Linear length</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLabel" name="linear_len_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="5" column="2"> + <widget class="QSlider" name="linear_len"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_8"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Logarithm base</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QLabel" name="log_base_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="6" column="2"> + <widget class="QSlider" name="log_base"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label_9"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Logarithm coefficient</string> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QLabel" name="log_c_label"> + <property name="minimumSize"> + <size> + <width>24</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>0</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="7" column="2"> + <widget class="QSlider" name="log_c"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="tickPosition"> + <enum>QSlider::TicksAbove</enum> + </property> + <property name="tickInterval"> + <number>25</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QFunctionConfigurator" name="relative_mode_gain" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>300</width> + <height>150</height> + </size> + </property> + <property name="colorBezier" stdset="0"> + <color> + <red>192</red> + <green>32</green> + <blue>8</blue> + </color> + </property> + <property name="colorBackground" stdset="0"> + <color> + <red>240</red> + <green>240</green> + <blue>240</blue> + </color> + </property> + <property name="is_preview_only" stdset="0"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + <zorder>relative_mode_gain</zorder> + <zorder>frame_3</zorder> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="tobii-eyex-res.qrc"/> + </resources> + <connections/> +</ui> diff --git a/tracker-tobii-eyex/tobii-eyex-res.qrc b/tracker-tobii-eyex/tobii-eyex-res.qrc new file mode 100644 index 00000000..e3395df9 --- /dev/null +++ b/tracker-tobii-eyex/tobii-eyex-res.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>images/tobii-eyex-logo.png</file> + </qresource> +</RCC> diff --git a/tracker-tobii-eyex/tobii-eyex.cpp b/tracker-tobii-eyex/tobii-eyex.cpp new file mode 100644 index 00000000..d668a4e2 --- /dev/null +++ b/tracker-tobii-eyex/tobii-eyex.cpp @@ -0,0 +1,445 @@ +#include "tobii-eyex.hpp" +#include <cstdlib> +#include <cstdio> +#include <cmath> +#include <functional> +#include <vector> +#include <algorithm> +#include <iterator> +#include <QDebug> +#include <QMutexLocker> +#include <QMessageBox> + +// XXX TODO whole opentrack needs different debug levels -sh 20160801 + +//#define TOBII_EYEX_DEBUG_PRINTF +#define TOBII_EYEX_VERBOSE_PRINTF + +#ifdef TOBII_EYEX_VERBOSE_PRINTF +# define dbg_verbose(msg) (qDebug() << "tobii-eyex:" << (msg)) +#else +# define dbg_verbose(msg) (QMessageLogger().noDebug() << (msg)) +#endif + +#ifdef TOBII_EYEX_DEBUG_PRINTF +# define dbg_debug(msg) (qDebug() << "tobii-eyex:" << (msg)) +#else +# define dbg_debug(msg) (QMessageLogger().noDebug() << (msg)) +#endif + +#define dbg_notice(msg) (qDebug() << "tobii-eyex:" << (msg)) + +std::atomic_flag tobii_eyex_tracker::atexit_done = ATOMIC_FLAG_INIT; + +static inline tobii_eyex_tracker& to_self(TX_USERPARAM param) +{ + return *reinterpret_cast<tobii_eyex_tracker*>(param); +} + +template<typename t> +static constexpr t clamp(t datum, t min, t max) +{ + return ((datum > max) ? max : ((datum < min) ? min : datum)); +} + +rel_settings::rel_settings() : + opts("tobii-eyex-relative-mode"), + speed(b, "speed", s(5, .1, 10)), + dz_end_pt(b, "deadzone-length", s(.05, 0, .2)), + expt_val(b, "exponent", s(1.75, 1.25, 2.25)), + log_base(b, "logarithm-base", s(1.75, 1.1, 5)) +{} + +tobii_eyex_tracker::tobii_eyex_tracker() : + dev_ctx(TX_EMPTY_HANDLE), + conn_state_changed_ticket(TX_INVALID_TICKET), + event_handler_ticket(TX_INVALID_TICKET), + state_snapshot(TX_EMPTY_HANDLE), + display_state(TX_EMPTY_HANDLE), + yaw(0), + pitch(0), + do_center(false) +{ +} + +void tobii_eyex_tracker::call_tx_deinit() +{ + dbg_notice("uninitialize in atexit at _fini time"); + (void) txUninitializeEyeX(); +} + +tobii_eyex_tracker::~tobii_eyex_tracker() +{ + dbg_verbose("dtor"); + + (void) txDisableConnection(dev_ctx); + (void) txReleaseObject(&state_snapshot); + + bool status = true; + status &= txShutdownContext(dev_ctx, TX_CLEANUPTIMEOUT_FORCEIMMEDIATE, TX_FALSE) == TX_RESULT_OK; + status &= txReleaseContext(&dev_ctx) == TX_RESULT_OK; + + // the API cleanup function needs to be called exactly once over image lifetime. + // client software communicates with a service and a desktop program. + // API is ambiguous as to what happens if the image doesn't call it or crashes. + if (!atexit_done.test_and_set()) + std::atexit(call_tx_deinit); + + if (!status) + dbg_notice("tobii-eyex: can't shutdown properly"); +} + +bool tobii_eyex_tracker::register_state_snapshot(TX_CONTEXTHANDLE dev_ctx, TX_HANDLE* state_snapshot_ptr) +{ + TX_HANDLE handle = TX_EMPTY_HANDLE; + TX_GAZEPOINTDATAPARAMS params = { TX_GAZEPOINTDATAMODE_LIGHTLYFILTERED }; + + bool status = true; + + status &= txCreateGlobalInteractorSnapshot(dev_ctx, client_id, state_snapshot_ptr, &handle) == TX_RESULT_OK; + status &= txCreateGazePointDataBehavior(handle, ¶ms) == TX_RESULT_OK; + + (void) txReleaseObject(&handle); + + return status; +} + +void tobii_eyex_tracker::process_display_state(TX_HANDLE display_state_handle) +{ + TX_SIZE2 screen_res; + + if (txGetStateValueAsSize2(display_state_handle, TX_STATEPATH_EYETRACKINGSCREENBOUNDS, &screen_res) == TX_RESULT_OK) + { + dbg_verbose("got display resolution") << screen_res.Width << screen_res.Height; + + QMutexLocker l(&global_state_mtx); + + dev_state.display_res_x = screen_res.Width; + dev_state.display_res_y = screen_res.Height; + } + else + dbg_notice("can't get display resolution"); +} + +void tobii_eyex_tracker::display_state_handler(TX_CONSTHANDLE async_data_handle, TX_USERPARAM param) +{ + tobii_eyex_tracker& self = to_self(param); + + TX_RESULT result = TX_RESULT_UNKNOWN; + TX_HANDLE state = TX_EMPTY_HANDLE; + + if (txGetAsyncDataResultCode(async_data_handle, &result) == TX_RESULT_OK && + txGetAsyncDataContent(async_data_handle, &state) == TX_RESULT_OK) + { + self.process_display_state(state); + txReleaseObject(&state); + } + else + dbg_notice("error in display state handler"); +} + +void tobii_eyex_tracker::snapshot_committed_handler(TX_CONSTHANDLE async_data_handle, TX_USERPARAM) +{ + TX_RESULT result = TX_RESULT_UNKNOWN; + txGetAsyncDataResultCode(async_data_handle, &result); + + if (!(result == TX_RESULT_OK || result == TX_RESULT_CANCELLED)) + dbg_notice("snapshot bad result code") << result; +} + +void tobii_eyex_tracker::connection_state_change_handler(TX_CONNECTIONSTATE state, TX_USERPARAM param) +{ + tobii_eyex_tracker& self = to_self(param); + + switch (state) + { + case TX_CONNECTIONSTATE_CONNECTED: + { + bool status = txCommitSnapshotAsync(self.state_snapshot, snapshot_committed_handler, param) == TX_RESULT_OK; + if (!status) + dbg_notice("connected but failed to initialize data stream"); + else + { + txGetStateAsync(self.dev_ctx, TX_STATEPATH_EYETRACKINGSCREENBOUNDS, display_state_handler, param); + dbg_notice("connected, data stream ok"); + } + } + break; + case TX_CONNECTIONSTATE_DISCONNECTED: + dbg_notice("connection state is now disconnected"); + break; + case TX_CONNECTIONSTATE_TRYINGTOCONNECT: + dbg_verbose("trying to establish connection"); + break; + case TX_CONNECTIONSTATE_SERVERVERSIONTOOLOW: + dbg_notice("installed driver version too low"); + break; + case TX_CONNECTIONSTATE_SERVERVERSIONTOOHIGH: + dbg_notice("new driver came up, we need to update sdk"); + break; + } +} + +void tobii_eyex_tracker::gaze_data_handler(TX_HANDLE gaze_data_handle) +{ + TX_GAZEPOINTDATAEVENTPARAMS params; + + if (txGetGazePointDataEventParams(gaze_data_handle, ¶ms) == TX_RESULT_OK) + { + { + QMutexLocker l(&global_state_mtx); + + if (params.Timestamp > dev_state.last_timestamp && + dev_state.display_res_x > 0 && + // the API allows for events outside screen bounds to e.g. detect looking at keyboard. + // closer to the screen bounds, the values get less accurate. + // ignore events outside the screen bounds. + params.X >= 0 && params.X < dev_state.display_res_x && + params.Y >= 0 && params.Y < dev_state.display_res_y) + { + dev_state.last_timestamp = params.Timestamp; + dev_state.px = params.X; + dev_state.py = params.Y; + +#ifdef TOBII_EYEX_DEBUG_PRINTF + char buf[256] = {0}; + (void) std::sprintf(buf, "gaze data: (%.1f, %.1f)", params.X, params.Y); + dbg_debug(buf); +#endif + + dev_state.fresh = true; + } + } + } + else + { + dbg_notice("failed to interpret gaze data event packet"); + } +} + +void tobii_eyex_tracker::event_handler(TX_CONSTHANDLE async_data_handle, TX_USERPARAM param) +{ + tobii_eyex_tracker& self = to_self(param); + + TX_HANDLE event_handle = TX_EMPTY_HANDLE; + TX_HANDLE behavior_handle = TX_EMPTY_HANDLE; + + txGetAsyncDataContent(async_data_handle, &event_handle); + + if (txGetEventBehavior(event_handle, &behavior_handle, TX_BEHAVIORTYPE_GAZEPOINTDATA) == TX_RESULT_OK) + { + self.gaze_data_handler(behavior_handle); + txReleaseObject(&behavior_handle); + } + + txReleaseObject(&event_handle); +} + +void tobii_eyex_tracker::start_tracker(QFrame*) +{ + dbg_verbose("start tracker"); + + bool status = true; + + status &= txInitializeEyeX(TX_EYEXCOMPONENTOVERRIDEFLAG_NONE, nullptr, nullptr, nullptr, nullptr) == TX_RESULT_OK; + status &= txCreateContext(&dev_ctx, TX_FALSE) == TX_RESULT_OK; + status &= register_state_snapshot(dev_ctx, &state_snapshot); + status &= txRegisterConnectionStateChangedHandler(dev_ctx, &conn_state_changed_ticket, connection_state_change_handler, reinterpret_cast<TX_USERPARAM>(this)) == TX_RESULT_OK; + status &= txRegisterEventHandler(dev_ctx, &event_handler_ticket, event_handler, reinterpret_cast<TX_USERPARAM>(this)) == TX_RESULT_OK; + status &= txEnableConnection(dev_ctx) == TX_RESULT_OK; + + if (!status) + dbg_verbose("connection can't be established. device missing?"); + else + dbg_verbose("api initialized"); +} + +// the gain function was prototyped in python with jupyter qtconsole. +// you can use qtconsole's inline matplotlib support to see the gain function. +// the `piecewise' function assumes monotonic growth or constant value for all functions. +/* + +from math import * +from itertools import izip +import matplotlib +import matplotlib.pyplot as plt + +try: + import IPython + IPython.get_ipython().magic(u'matplotlib inline') +except: + pass + +def frange(from_, to_, step_=1e-4): + i = from_ + while True: + yield i + i += step_ + if i >= to_: + break + +def plot_fn(fn, from_=0., to_=1., step=None): + if step is None: + step = max(1e-4, (to_-from_)*1e-4) + xs = [i for i in frange(from_, to_, step)] + plt.plot(xs, map(fn, xs)) + +def piecewise(x, funs, bounds): + y = 0. + last_bound = 0. + norm = 0. + for fun in funs: + norm += fun(1.) + for fun, bound in izip(funs, bounds): + if x > bound: + y += fun(1.) + else: + b = bound - last_bound + x_ = (x - last_bound) / b + y += fun(x_) + break + last_bound = bound + return y / norm + +def f(x): return x**1.75 +def g(x): return 1.75*1.75*x +def h(x): return log(1+x)/log(2.5) +def zero(x): return 0. + +plot_fn(lambda x: piecewise(x, [zero, f, g, h], [.05, .25, .7, 1.])) + +*/ + +template<typename funs_seq, typename bounds_seq> +tobii_eyex_tracker::num tobii_eyex_tracker::piecewise(num x, const funs_seq& funs, const bounds_seq& bounds) +{ + using fn = std::function<num(num)>; + + auto funs_it = std::begin(funs); + auto bounds_it = std::begin(bounds); + + auto funs_end = std::end(funs); + auto bounds_end = std::end(bounds); + + num norm = 0; + + for (const fn& f : funs) + { + norm += f(1); + } + + norm = std::max(num(1e-4), norm); + + num last_bound = 0, y = 0; + + for (; + bounds_it != bounds_end && funs_it != funs_end; + bounds_it++, funs_it++) + { + const fn& fun = *funs_it; + const num bound = *bounds_it; + + if (x > bound) + { + y += fun(1); + last_bound = bound; + } + else + { + const num b = bound - last_bound; + // rescale x to 0->1 + const num x_ = (x - last_bound) / b; + y += fun(x_); + break; + } + } + return clamp(y / norm, num(0), num(1)); +} + +tobii_eyex_tracker::num tobii_eyex_tracker::gain(num x_) +{ + const num x = std::fabs(x_); + + static const fun_t funs[] = + { + [](num) -> num { return num(0); }, + [](num x) -> num { return std::pow(x, 1.1)*.08; }, + [](num x) -> num { return x*.5; }, + }; + + static constexpr num dz_l = .1, expt_l = .3; + + static constexpr num ends[] = + { + dz_l, + expt_l, + 1, + }; + + const num ret = piecewise(x, funs, ends); + return std::copysign(clamp(ret, num(0), num(1)), x_); +} + +void tobii_eyex_tracker::data(double* data) +{ + TX_REAL px, py, dw, dh, x_, y_; + bool fresh; + + { + QMutexLocker l(&global_state_mtx); + + if (!dev_state.is_valid()) + return; + + px = dev_state.px; + py = dev_state.py; + dw = dev_state.display_res_x; + dh = dev_state.display_res_y; + + fresh = dev_state.fresh; + dev_state.fresh = false; + } + + x_ = (px-dw/2.) / (dw/2.); + y_ = (py-dh/2.) / (dh/2.); + + data[TX] = x_ * 50; + data[TY] = y_ * -50; + + if (fresh) + { + const double dt = t.elapsed_seconds(); + t.start(); + // XXX TODO make slider + static constexpr double v = 300; + + const double x = gain(x_); + const double y = gain(y_); + + const double yaw_delta = (x * v) * dt; + const double pitch_delta = (y * -v) * dt; + + yaw += yaw_delta; + pitch += pitch_delta; + + yaw = clamp(yaw, -180., 180.); + pitch = clamp(pitch, -60., 60.); + } + + if (do_center) + { + do_center = false; + yaw = 0; + pitch = 0; + } + + data[Yaw] = yaw; + data[Pitch] = pitch; + data[Roll] = 0; + data[TZ] = 0; // XXX TODO + + // tan(x) in 0->.7 is almost linear. we don't need to adjust. + // .7 is 40 degrees which is already quite a lot from the monitor. +} + +OPENTRACK_DECLARE_TRACKER(tobii_eyex_tracker, tobii_eyex_dialog, tobii_eyex_metadata) diff --git a/tracker-tobii-eyex/tobii-eyex.hpp b/tracker-tobii-eyex/tobii-eyex.hpp new file mode 100644 index 00000000..d8bb3606 --- /dev/null +++ b/tracker-tobii-eyex/tobii-eyex.hpp @@ -0,0 +1,135 @@ +#pragma once + +/* Copyright (c) 2016 Stanislaw Halik <sthalik@misaki.pl> + + * 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 <EyeX.h> + +#include "ui_tobii-eyex-dialog.h" +#include "opentrack/plugin-api.hpp" +#include "opentrack-compat/options.hpp" +using namespace options; +#include "opentrack-compat/timer.hpp" +#include "spline-widget/functionconfig.h" +#include "spline-widget/qfunctionconfigurator.h" + +#include <atomic> +#include <QObject> +#include <QMutex> + +enum tobii_mode +{ + tobii_relative, + tobii_absolute, +}; + +struct rel_settings : public QObject, public opts +{ + using s = slider_value; + value<slider_value> speed, dz_end_pt, expt_val, log_base; + rel_settings(); +private: + // linear coefficient to be the same as exponent + Map spline; +}; + +struct settings : public opts +{ + value<tobii_mode> mode; + settings() : + opts("tobii-eyex"), + mode(b, "mode", tobii_relative) + {} +}; + +class tobii_eyex_tracker : public ITracker +{ +public: + tobii_eyex_tracker(); + ~tobii_eyex_tracker() override; + void start_tracker(QFrame *) override; + void data(double *data) override; + bool center() override + { + do_center = true; + return true; + } +private: + static constexpr const char* client_id = "opentrack-tobii-eyex"; + + static void call_tx_deinit(); + + static bool register_state_snapshot(TX_CONTEXTHANDLE ctx, TX_HANDLE* state_snapshot_ptr); + static std::atomic_flag atexit_done; + static void TX_CALLCONVENTION connection_state_change_handler(TX_CONNECTIONSTATE state, TX_USERPARAM param); + static void TX_CALLCONVENTION event_handler(TX_CONSTHANDLE async_data_handle, TX_USERPARAM param); + void gaze_data_handler(TX_HANDLE gaze_data_handle); + static void TX_CALLCONVENTION snapshot_committed_handler(TX_CONSTHANDLE async_data_handle, TX_USERPARAM param); + static void TX_CALLCONVENTION display_state_handler(TX_CONSTHANDLE async_data_handle, TX_USERPARAM param); + void process_display_state(TX_HANDLE display_state_handle); + + using num = double; + + template<typename funs_seq, typename bounds_seq> + static num piecewise(num x, const funs_seq& funs, const bounds_seq& bounds); + + using fun_t = std::function<num(num)>; + + num gain(num x); + + settings s; + rel_settings rel_s; + + TX_CONTEXTHANDLE dev_ctx; + TX_TICKET conn_state_changed_ticket; + TX_TICKET event_handler_ticket; + TX_HANDLE state_snapshot; + TX_HANDLE display_state; + + QMutex global_state_mtx; + + Timer t; + + struct state + { + TX_REAL display_res_x, display_res_y; + TX_REAL px, py; + TX_REAL last_timestamp; + bool fresh; + + state() : display_res_x(-1), display_res_y(-1), px(-1), py(-1), last_timestamp(0), fresh(false) {} + bool is_valid() const { return !(display_res_x < 0 || px < 0); } + } dev_state; + + double yaw, pitch; + volatile bool do_center; +}; + +class tobii_eyex_dialog final : public ITrackerDialog +{ + Q_OBJECT +public: + tobii_eyex_dialog(); + void register_tracker(ITracker *) override {} + void unregister_tracker() override {} +private: + Ui::tobii_eyex_dialog_widgets ui; + settings s; + rel_settings rs; +private slots: + void do_ok(); + void do_cancel(); +}; + +class tobii_eyex_metadata : public Metadata +{ +public: + QString name() { return QString("Tobii EyeX"); } + QIcon icon() { return QIcon(":/images/tobii-eyex-logo.png"); } +}; + |