summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorStanislaw Halik <sthalik@misaki.pl>2023-09-26 15:45:36 +0200
committerGitHub <noreply@github.com>2023-09-26 15:45:36 +0200
commita123bb65e10dab56f32f0a4ed015b366d23615da (patch)
treedf9aa3d97506834c4389e7f2dc738e396155df94
parent6f836ab2ab0edb296e0dc3884983cf6b956fce68 (diff)
parenta462b5411e4c6e958adb3b2d1a75657d56522942 (diff)
Merge pull request #1724 from khoanguyen-3fc/feature/tobii-tracker
-rw-r--r--tracker-tobii/CMakeLists.txt17
-rw-r--r--tracker-tobii/ftnoir_tracker_tobii.cpp65
-rw-r--r--tracker-tobii/ftnoir_tracker_tobii.h50
-rw-r--r--tracker-tobii/ftnoir_tracker_tobii_controls.ui56
-rw-r--r--tracker-tobii/ftnoir_tracker_tobii_dialog.cpp19
-rw-r--r--tracker-tobii/lang/nl_NL.ts12
-rw-r--r--tracker-tobii/lang/ru_RU.ts12
-rw-r--r--tracker-tobii/lang/stub.ts12
-rw-r--r--tracker-tobii/lang/zh_CN.ts12
-rw-r--r--tracker-tobii/thread.cpp109
-rw-r--r--tracker-tobii/thread.hpp35
-rw-r--r--tracker-tobii/tobii.cpp121
-rw-r--r--tracker-tobii/tobii.h61
-rw-r--r--tracker-tobii/tobii.qrc5
-rw-r--r--tracker-tobii/tobii.ui67
-rw-r--r--tracker-tobii/tobii_dialog.cpp20
-rw-r--r--tracker-tobii/tobii_logo.pngbin0 -> 1768 bytes
17 files changed, 313 insertions, 360 deletions
diff --git a/tracker-tobii/CMakeLists.txt b/tracker-tobii/CMakeLists.txt
index 8551c8fa..c70a874c 100644
--- a/tracker-tobii/CMakeLists.txt
+++ b/tracker-tobii/CMakeLists.txt
@@ -1,17 +1,14 @@
-# https://developer.tobii.com/download-packages/stream-engine-4-1-0-for-windows-x86
-# https://developer.tobii.com/download-packages/stream-engine-4-1-0-for-windows-x64
-set(SDK_TOBII "" CACHE PATH "Tobii SDK path")
-if(SDK_TOBII)
+# https://developer.tobii.com/download-packages/tobii-xr-native-sdk/
+set(SDK_TOBII "" CACHE PATH "Tobii Stream Engine path")
+if(WIN32 AND SDK_TOBII)
otr_module(tracker-tobii)
+
target_include_directories(${self} SYSTEM PRIVATE "${SDK_TOBII}/include")
+ target_link_directories(${self} PRIVATE "${SDK_TOBII}/lib/tobii")
set(dll "${SDK_TOBII}/lib/tobii/tobii_stream_engine.dll")
- set(lib "${SDK_TOBII}/lib/tobii/tobii_stream_engine.lib")
-
- message(${self})
- message(${dll})
- message(${lib})
+ set(lib tobii_stream_engine.lib)
- install(FILES ${dll} DESTINATION ${opentrack-hier-pfx})
target_link_libraries(${self} ${lib})
+ install(FILES ${dll} DESTINATION ${opentrack-libexec})
endif()
diff --git a/tracker-tobii/ftnoir_tracker_tobii.cpp b/tracker-tobii/ftnoir_tracker_tobii.cpp
deleted file mode 100644
index 595cf410..00000000
--- a/tracker-tobii/ftnoir_tracker_tobii.cpp
+++ /dev/null
@@ -1,65 +0,0 @@
-#include "ftnoir_tracker_tobii.h"
-#include "api/plugin-api.hpp"
-#include "compat/math.hpp"
-
-tobii::~tobii() = default;
-
-module_status tobii::start_tracker(QFrame*)
-{
- t.start();
- return status_ok();
-}
-
-void tobii::data(double *data)
-{
- if (t.head_pose) {
- tobii_head_pose_t p = *t.head_pose;
- if (p.position_validity == TOBII_VALIDITY_VALID) {
- if (center_pose.position_validity == TOBII_VALIDITY_VALID) {
- p.position_xyz[0] = p.position_xyz[0] - center_pose.position_xyz[0];
- p.position_xyz[1] = p.position_xyz[1] - center_pose.position_xyz[1];
- p.position_xyz[2] = p.position_xyz[2] - center_pose.position_xyz[2];
- }
- else {
- center_pose = p;
- }
- data[0] = std::clamp(p.position_xyz[0] * 30.0 / 300.0, -30.0, 30.0);
- data[1] = std::clamp(p.position_xyz[1] * 30.0 / 300.0, -30.0, 30.0);
- data[2] = std::clamp(p.position_xyz[2] * 30.0 / 300.0, -30.0, 30.0);
- }
-
- double max_yaw = 90.0;
- if (p.rotation_validity_xyz[1] == TOBII_VALIDITY_VALID) {
- if (center_pose.rotation_validity_xyz[1] == TOBII_VALIDITY_VALID) {
- p.rotation_xyz[1] = p.rotation_xyz[1] - center_pose.rotation_xyz[1];
- }
- data[3] = std::clamp(p.rotation_xyz[1] * 100.0 * max_yaw / 90.0, -max_yaw, max_yaw);
- }
-
- double max_pitch = 90.0;
- if (p.rotation_validity_xyz[0] == TOBII_VALIDITY_VALID) {
- if (center_pose.rotation_validity_xyz[0] == TOBII_VALIDITY_VALID) {
- p.rotation_xyz[0] = p.rotation_xyz[0] - center_pose.rotation_xyz[0];
- }
- data[4] = std::clamp(p.rotation_xyz[0] * 100.0 * max_pitch / 90.0, -max_pitch, max_pitch);
- }
-
- double max_roll = 90.0;
- if (p.rotation_validity_xyz[2] == TOBII_VALIDITY_VALID) {
- if (center_pose.rotation_validity_xyz[2] == TOBII_VALIDITY_VALID) {
- p.rotation_xyz[2] = p.rotation_xyz[2] - center_pose.rotation_xyz[2];
- }
- data[5] = std::clamp(p.rotation_xyz[2] * 100.0 * max_roll / 90.0, -max_roll, max_roll);
- }
- }
-}
-
-bool tobii::center ()
-{
- if (t.head_pose) {
- center_pose = *t.head_pose;
- }
- return false;
-}
-
-OPENTRACK_DECLARE_TRACKER(tobii, dialog_tobii, tobiiDll)
diff --git a/tracker-tobii/ftnoir_tracker_tobii.h b/tracker-tobii/ftnoir_tracker_tobii.h
deleted file mode 100644
index bd7a04a2..00000000
--- a/tracker-tobii/ftnoir_tracker_tobii.h
+++ /dev/null
@@ -1,50 +0,0 @@
-#pragma once
-#include "ui_ftnoir_tracker_tobii_controls.h"
-#include <QComboBox>
-#include <QCheckBox>
-#include <QSpinBox>
-#include <QMessageBox>
-#include <QSettings>
-#include <QList>
-#include <QFrame>
-#include <QStringList>
-#include <cmath>
-#include "api/plugin-api.hpp"
-#include "options/options.hpp"
-
-#include "thread.hpp"
-
-class tobii : public ITracker
-{
-public:
- ~tobii();
- module_status start_tracker(QFrame*) override;
- void data(double* data) override;
- virtual bool center() override;
-private:
- tobii_thread t;
- tobii_head_pose_t center_pose;
-};
-
-class dialog_tobii: public ITrackerDialog
-{
- Q_OBJECT
-public:
- dialog_tobii();
- ~dialog_tobii() = default;
- void register_tracker(ITracker *) {}
- void unregister_tracker() {}
- Ui::UITobiiControls ui;
- tobii* tracker;
-private slots:
- void doOK();
- void doCancel();
-};
-
-class tobiiDll : public Metadata
-{
- Q_OBJECT
-
- QString name() { return tr("Tobii input"); }
- QIcon icon() { return QIcon(":/images/opentrack.png"); }
-};
diff --git a/tracker-tobii/ftnoir_tracker_tobii_controls.ui b/tracker-tobii/ftnoir_tracker_tobii_controls.ui
deleted file mode 100644
index 6cd00121..00000000
--- a/tracker-tobii/ftnoir_tracker_tobii_controls.ui
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>UITobiiControls</class>
- <widget class="QWidget" name="UITobiiControls">
- <property name="windowModality">
- <enum>Qt::NonModal</enum>
- </property>
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>498</width>
- <height>303</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Tobii settings</string>
- </property>
- <property name="windowIcon">
- <iconset>
- <normaloff>../gui/images/opentrack.png</normaloff>../gui/images/opentrack.png
- </iconset>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="leftMargin">
- <number>12</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>12</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <widget class="QDialogButtonBox" name="buttonBox">
- <property name="standardButtons">
- <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>buttonBox</tabstop>
- </tabstops>
- <resources/>
- <connections/>
- <slots>
- <slot>startEngineClicked()</slot>
- <slot>stopEngineClicked()</slot>
- <slot>cameraSettingsClicked()</slot>
- </slots>
-</ui>
diff --git a/tracker-tobii/ftnoir_tracker_tobii_dialog.cpp b/tracker-tobii/ftnoir_tracker_tobii_dialog.cpp
deleted file mode 100644
index 5cd4bad0..00000000
--- a/tracker-tobii/ftnoir_tracker_tobii_dialog.cpp
+++ /dev/null
@@ -1,19 +0,0 @@
-#include "ftnoir_tracker_tobii.h"
-#include "api/plugin-api.hpp"
-
-dialog_tobii::dialog_tobii() : tracker(nullptr)
-{
- ui.setupUi( this );
-
- // Connect Qt signals to member-functions
- connect(ui.buttonBox, SIGNAL(accepted()), this, SLOT(doOK()));
- connect(ui.buttonBox, SIGNAL(rejected()), this, SLOT(doCancel()));
-}
-
-void dialog_tobii::doOK() {
- close();
-}
-
-void dialog_tobii::doCancel() {
- close();
-}
diff --git a/tracker-tobii/lang/nl_NL.ts b/tracker-tobii/lang/nl_NL.ts
index 45e4974f..538a31f1 100644
--- a/tracker-tobii/lang/nl_NL.ts
+++ b/tracker-tobii/lang/nl_NL.ts
@@ -2,16 +2,20 @@
<!DOCTYPE TS>
<TS version="2.1" language="nl_NL">
<context>
- <name>UITobiiControls</name>
+ <name>tobii_metadata</name>
<message>
- <source>Tobii settings</source>
+ <source>Tobii Eye Tracker</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>tobiiDll</name>
+ <name>tobii_ui</name>
<message>
- <source>Tobii input</source>
+ <source>Tobii Eye Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Please make sure the Tobii Experience application is running and tracking is active.</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/tracker-tobii/lang/ru_RU.ts b/tracker-tobii/lang/ru_RU.ts
index ee73b375..b86ba010 100644
--- a/tracker-tobii/lang/ru_RU.ts
+++ b/tracker-tobii/lang/ru_RU.ts
@@ -2,16 +2,20 @@
<!DOCTYPE TS>
<TS version="2.1" language="ru_RU">
<context>
- <name>UITobiiControls</name>
+ <name>tobii_metadata</name>
<message>
- <source>Tobii settings</source>
+ <source>Tobii Eye Tracker</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>tobiiDll</name>
+ <name>tobii_ui</name>
<message>
- <source>Tobii input</source>
+ <source>Tobii Eye Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Please make sure the Tobii Experience application is running and tracking is active.</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/tracker-tobii/lang/stub.ts b/tracker-tobii/lang/stub.ts
index cd8a2bdc..566900a3 100644
--- a/tracker-tobii/lang/stub.ts
+++ b/tracker-tobii/lang/stub.ts
@@ -2,16 +2,20 @@
<!DOCTYPE TS>
<TS version="2.1">
<context>
- <name>UITobiiControls</name>
+ <name>tobii_metadata</name>
<message>
- <source>Tobii settings</source>
+ <source>Tobii Eye Tracker</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>tobiiDll</name>
+ <name>tobii_ui</name>
<message>
- <source>Tobii input</source>
+ <source>Tobii Eye Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Please make sure the Tobii Experience application is running and tracking is active.</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/tracker-tobii/lang/zh_CN.ts b/tracker-tobii/lang/zh_CN.ts
index 9ea8f636..cbf60176 100644
--- a/tracker-tobii/lang/zh_CN.ts
+++ b/tracker-tobii/lang/zh_CN.ts
@@ -2,16 +2,20 @@
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
- <name>UITobiiControls</name>
+ <name>tobii_metadata</name>
<message>
- <source>Tobii settings</source>
+ <source>Tobii Eye Tracker</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>tobiiDll</name>
+ <name>tobii_ui</name>
<message>
- <source>Tobii input</source>
+ <source>Tobii Eye Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Please make sure the Tobii Experience application is running and tracking is active.</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/tracker-tobii/thread.cpp b/tracker-tobii/thread.cpp
deleted file mode 100644
index bdd48125..00000000
--- a/tracker-tobii/thread.cpp
+++ /dev/null
@@ -1,109 +0,0 @@
-#include "thread.hpp"
-#include "compat/sleep.hpp"
-
-tobii_thread::~tobii_thread()
-{
- exit_thread = true;
- wait();
-
- if (device) tobii_device_destroy(device);
- if (api) tobii_api_destroy(api);
-
- quit();
-}
-
-void tobii_thread::run()
-{
- /* See https://developer.tobii.com/consumer-eye-trackers/stream-engine/ */
- if (tobii_api_create(&api, nullptr, nullptr) != TOBII_ERROR_NO_ERROR)
- {
- error_last = "Failed to initialize the Tobii Stream Engine API.";
- exit_thread = true;
- }
-
- std::vector<std::string> devices;
- if (tobii_enumerate_local_device_urls(api,
- [](char const* url, void* user_data)
- {
- auto list = (std::vector<std::string>*) user_data;
- list->push_back(url);
- }, &devices) != TOBII_ERROR_NO_ERROR)
- {
- error_last = "Failed to enumerate devices.";
- exit_thread = true;
- }
-
- if (devices.size() == 0)
- {
- tobii_api_destroy(api);
- error_last = "No stream engine compatible device(s) found.";
- exit_thread = true;
- }
- std::string selected_device = devices[0];
-
- unsigned int retry = 0;
- tobii_error_t tobii_error = TOBII_ERROR_NO_ERROR;
- do
- {
- tobii_error = tobii_device_create(api, selected_device.c_str(), TOBII_FIELD_OF_USE_INTERACTIVE, &device);
- if ((tobii_error != TOBII_ERROR_CONNECTION_FAILED) && (tobii_error != TOBII_ERROR_FIRMWARE_UPGRADE_IN_PROGRESS)) break;
- portable::sleep(interval);
- ++retry;
- } while (retry < retries);
- if (tobii_error != TOBII_ERROR_NO_ERROR) {
- tobii_api_destroy(api);
- error_last = "Failed to connect to device.";
- exit_thread = true;
- }
-
- if (tobii_head_pose_subscribe(device, [](tobii_head_pose_t const* head_pose, void* user_data) {
-
- if ((*head_pose).position_validity != TOBII_VALIDITY_VALID
- || (*head_pose).rotation_validity_xyz[0] != TOBII_VALIDITY_VALID
- || (*head_pose).rotation_validity_xyz[1] != TOBII_VALIDITY_VALID
- || (*head_pose).rotation_validity_xyz[2] != TOBII_VALIDITY_VALID) return;
-
- tobii_head_pose_t* tobii_head_pose_storage = (tobii_head_pose_t*)user_data;
- *tobii_head_pose_storage = *head_pose;
-
- }, head_pose) != TOBII_ERROR_NO_ERROR)
- {
- error_last = "Failed to subscribe to head pose stream.";
- exit_thread = true;
- }
-
- tobii_error = TOBII_ERROR_NO_ERROR;
- while (!exit_thread)
- {
- tobii_error = tobii_wait_for_callbacks(1, &device);
- if (tobii_error == TOBII_ERROR_TIMED_OUT) continue;
- else if (tobii_error != TOBII_ERROR_NO_ERROR)
- {
- error_last = "tobii_wait_for_callbacks failed.";
- }
-
- tobii_error = tobii_device_process_callbacks(device);
- if (tobii_error == TOBII_ERROR_CONNECTION_FAILED)
- {
- unsigned int retry = 0;
- auto tobii_error = TOBII_ERROR_NO_ERROR;
- do
- {
- tobii_error = tobii_device_reconnect(device);
- if ((tobii_error != TOBII_ERROR_CONNECTION_FAILED) && (tobii_error != TOBII_ERROR_FIRMWARE_UPGRADE_IN_PROGRESS)) break;
- portable::sleep(interval);
- ++retry;
- } while (retry < retries);
- if (tobii_error != TOBII_ERROR_NO_ERROR)
- {
- error_last = "Connection was lost and reconnection failed.";
- exit_thread = true;
- }
- continue;
- }
- else if (tobii_error != TOBII_ERROR_NO_ERROR)
- {
- error_last = "tobii_device_process_callbacks failed.";
- }
- }
-}
diff --git a/tracker-tobii/thread.hpp b/tracker-tobii/thread.hpp
deleted file mode 100644
index d311db87..00000000
--- a/tracker-tobii/thread.hpp
+++ /dev/null
@@ -1,35 +0,0 @@
-#pragma once
-
-#include <QThread>
-#include <QCoreApplication>
-
-#include <tobii/tobii.h>
-#include <tobii/tobii_streams.h>
-#include <atomic>
-#include <vector>
-#include <string>
-
-class tobii_thread : public QThread
-{
- Q_OBJECT
- void run() override;
-
-public:
- tobii_thread()
- {
- head_pose = new tobii_head_pose_t();
- }
- ~tobii_thread() override;
-
- tobii_head_pose_t* head_pose;
-
-private:
- tobii_api_t* api;
- tobii_device_t* device;
-
- static constexpr unsigned int retries = 300;
- static constexpr unsigned int interval = 100;
-
- QString error_last = "";
- std::atomic<bool> exit_thread = false;
-};
diff --git a/tracker-tobii/tobii.cpp b/tracker-tobii/tobii.cpp
new file mode 100644
index 00000000..e25cf52a
--- /dev/null
+++ b/tracker-tobii/tobii.cpp
@@ -0,0 +1,121 @@
+/* Copyright (c) 2023, Khoa Nguyen <khoanguyen@3forcom.com>
+
+ * 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 "tobii.h"
+#include "compat/math-imports.hpp"
+
+#include <QMutexLocker>
+
+static constexpr double rad_to_deg = 180.0 * M_1_PI;
+static constexpr double mm_to_cm = 0.1;
+
+static void url_receiver(char const* url, void* user_data)
+{
+ char* buffer = (char*)user_data;
+ if (*buffer != '\0')
+ return; // only keep first value
+
+ if (strlen(url) < 256)
+ strcpy(buffer, url);
+}
+
+static void head_pose_callback(tobii_head_pose_t const* head_pose, void* user_data)
+{
+ // Store the latest head pose data in the supplied storage
+ tobii_head_pose_t* head_pose_storage = (tobii_head_pose_t*)user_data;
+ *head_pose_storage = *head_pose;
+}
+
+tobii_tracker::tobii_tracker() = default;
+
+tobii_tracker::~tobii_tracker()
+{
+ QMutexLocker lck(&mtx);
+ if (device)
+ {
+ tobii_head_pose_unsubscribe(device);
+ tobii_device_destroy(device);
+ }
+ if (api)
+ {
+ tobii_api_destroy(api);
+ }
+}
+
+module_status tobii_tracker::start_tracker(QFrame*)
+{
+ QMutexLocker lck(&mtx);
+ tobii_error_t tobii_error = tobii_api_create(&api, nullptr, nullptr);
+ if (tobii_error != TOBII_ERROR_NO_ERROR)
+ {
+ return error("Failed to initialize the Tobii Stream Engine API.");
+ }
+
+ char url[256] = { 0 };
+ tobii_error = tobii_enumerate_local_device_urls(api, url_receiver, url);
+ if (tobii_error != TOBII_ERROR_NO_ERROR || url[0] == '\0')
+ {
+ tobii_api_destroy(api);
+ return error("No stream engine compatible device(s) found.");
+ }
+
+ tobii_error = tobii_device_create(api, url, TOBII_FIELD_OF_USE_INTERACTIVE, &device);
+ if (tobii_error != TOBII_ERROR_NO_ERROR)
+ {
+ tobii_api_destroy(api);
+ return error(QString("Failed to connect to %1.").arg(url));
+ }
+
+ tobii_error = tobii_head_pose_subscribe(device, head_pose_callback, &latest_head_pose);
+ if (tobii_error != TOBII_ERROR_NO_ERROR)
+ {
+ tobii_device_destroy(device);
+ tobii_api_destroy(api);
+ return error("Failed to subscribe to head pose stream.");
+ }
+
+ return status_ok();
+}
+
+void tobii_tracker::data(double* data)
+{
+ QMutexLocker lck(&mtx);
+ tobii_error_t tobii_error = tobii_device_process_callbacks(device);
+ if (tobii_error != TOBII_ERROR_NO_ERROR)
+ {
+ return;
+ }
+
+ // Tobii coordinate system is different from OpenTrack's
+ // Tobii: +x is to the right, +y is up, +z is towards the user
+ // Rotation xyz is in radians, x is pitch, y is yaw, z is roll
+
+ if (latest_head_pose.position_validity == TOBII_VALIDITY_VALID)
+ {
+ data[TX] = -latest_head_pose.position_xyz[0] * mm_to_cm;
+ data[TY] = latest_head_pose.position_xyz[1] * mm_to_cm;
+ data[TZ] = latest_head_pose.position_xyz[2] * mm_to_cm;
+ }
+
+ if (latest_head_pose.rotation_validity_xyz[0] == TOBII_VALIDITY_VALID)
+ {
+ data[Pitch] = latest_head_pose.rotation_xyz[0] * rad_to_deg;
+ }
+
+ if (latest_head_pose.rotation_validity_xyz[1] == TOBII_VALIDITY_VALID)
+ {
+ data[Yaw] = -latest_head_pose.rotation_xyz[1] * rad_to_deg;
+ }
+
+ if (latest_head_pose.rotation_validity_xyz[2] == TOBII_VALIDITY_VALID)
+ {
+ data[Roll] = latest_head_pose.rotation_xyz[2] * rad_to_deg;
+ }
+}
+
+OPENTRACK_DECLARE_TRACKER(tobii_tracker, tobii_dialog, tobii_metadata)
diff --git a/tracker-tobii/tobii.h b/tracker-tobii/tobii.h
new file mode 100644
index 00000000..414f9f64
--- /dev/null
+++ b/tracker-tobii/tobii.h
@@ -0,0 +1,61 @@
+/* Copyright (c) 2023, Khoa Nguyen <khoanguyen@3forcom.com>
+
+ * 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 "ui_tobii.h"
+
+#include <tobii/tobii.h>
+#include <tobii/tobii_streams.h>
+
+#include <QMutex>
+
+class tobii_tracker : public ITracker
+{
+public:
+ tobii_tracker();
+ ~tobii_tracker() override;
+ module_status start_tracker(QFrame*) override;
+ void data(double* data) override;
+
+private:
+ tobii_api_t* api = nullptr;
+ tobii_device_t* device = nullptr;
+
+ tobii_head_pose_t latest_head_pose{
+ .timestamp_us = 0LL,
+ .position_validity = TOBII_VALIDITY_INVALID,
+ .position_xyz = { 0.f, 0.f, 0.f },
+ .rotation_validity_xyz = { TOBII_VALIDITY_INVALID, TOBII_VALIDITY_INVALID, TOBII_VALIDITY_INVALID },
+ .rotation_xyz = { 0.f, 0.f, 0.f },
+ };
+
+ QMutex mtx;
+};
+
+class tobii_dialog : public ITrackerDialog
+{
+ Q_OBJECT
+
+ Ui::tobii_ui ui;
+
+public:
+ tobii_dialog();
+
+private slots:
+ void doOK();
+ void doCancel();
+};
+
+class tobii_metadata : public Metadata
+{
+ Q_OBJECT
+
+ QString name() override { return tr("Tobii Eye Tracker"); }
+ QIcon icon() override { return QIcon(":/images/tobii_logo.png"); }
+};
diff --git a/tracker-tobii/tobii.qrc b/tracker-tobii/tobii.qrc
new file mode 100644
index 00000000..3e785ef7
--- /dev/null
+++ b/tracker-tobii/tobii.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource prefix="/images">
+ <file>tobii_logo.png</file>
+ </qresource>
+</RCC>
diff --git a/tracker-tobii/tobii.ui b/tracker-tobii/tobii.ui
new file mode 100644
index 00000000..71b29c12
--- /dev/null
+++ b/tracker-tobii/tobii.ui
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>tobii_ui</class>
+ <widget class="QWidget" name="tobii_ui">
+ <property name="windowModality">
+ <enum>Qt::NonModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>278</width>
+ <height>58</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Tobii Eye Tracker</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/images/tobii_logo.png</normaloff>:/images/tobii_logo.png</iconset>
+ </property>
+ <property name="layoutDirection">
+ <enum>Qt::LeftToRight</enum>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Please make sure the Tobii Experience application is running and tracking is active.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources>
+ <include location="tobii.qrc"/>
+ </resources>
+ <connections/>
+ <slots>
+ <slot>startEngineClicked()</slot>
+ <slot>stopEngineClicked()</slot>
+ <slot>cameraSettingsClicked()</slot>
+ </slots>
+</ui>
diff --git a/tracker-tobii/tobii_dialog.cpp b/tracker-tobii/tobii_dialog.cpp
new file mode 100644
index 00000000..689cae38
--- /dev/null
+++ b/tracker-tobii/tobii_dialog.cpp
@@ -0,0 +1,20 @@
+#include "tobii.h"
+
+tobii_dialog::tobii_dialog() // NOLINT(cppcoreguidelines-pro-type-member-init)
+{
+ ui.setupUi(this);
+
+ connect(ui.buttonBox, SIGNAL(accepted()), this, SLOT(doOK()));
+ connect(ui.buttonBox, SIGNAL(rejected()), this, SLOT(doCancel()));
+}
+
+void tobii_dialog::doOK()
+{
+ // s.b->save();
+ close();
+}
+
+void tobii_dialog::doCancel()
+{
+ close();
+}
diff --git a/tracker-tobii/tobii_logo.png b/tracker-tobii/tobii_logo.png
new file mode 100644
index 00000000..3502b37b
--- /dev/null
+++ b/tracker-tobii/tobii_logo.png
Binary files differ