├── .gitignore
├── .gitmodules
├── .jenkins
└── application
│ ├── Jenkinsfile
│ └── config_release.json
├── Documentation
├── Images
│ └── CodeStructure.png
└── ReleaseNotes.md
├── LICENSE
├── README.md
└── Src
├── AndroidPlugin
├── EmotivCortexLib
│ └── build.gradle
├── app
│ ├── build.gradle
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
├── gradle.properties
└── unityplugin
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── emotiv
│ └── unityplugin
│ ├── CortexConnection.java
│ ├── CortexConnectionInterface.java
│ ├── CortexLibActivity.java
│ ├── CortexLibManager.java
│ └── JavaLogInterface.java
├── Authorizer.cs
├── BCIGameItf.cs
├── BCITraining.cs
├── BandPowerDataBuffer.cs
├── BufferStream.cs
├── Config.cs
├── CortexApi
├── CortexLib.cs
├── CortexLogEventHandler.cs
├── CortexStartedEventHandler.cs
├── EmbeddedCortexClientNative.cs
├── EmotivCortexLib.cs
├── EmotivCortexLibPINVOKE.cs
└── ResponseHandlerCpp.cs
├── CortexClient.cs
├── DataBuffer.cs
├── DataStreamManager.cs
├── DataStreamProcess.cs
├── DevDataBuffer.cs
├── Editor
└── ExportUnityPackage.cs
├── EegMotionDataBuffer.cs
├── EmbeddedCortexClient.cs
├── EmotivUnityItf.cs
├── Headset.cs
├── HeadsetFinder.cs
├── IdentityModel
└── IdentityModel.dll
├── IosPlugin
├── CortexLibIosEmbeddedConnection.h
├── CortexLibIosEmbeddedConnection.m
├── CortexLibIosWrapper.m
└── README.md
├── JsonNet
└── Newtonsoft.Json.dll
├── MentalStateModel.cs
├── MyLogger.cs
├── PMDataBuffer.cs
├── PostProcessBuild
├── PostProcessBuild.Editor.asmdef
└── PostProcessBuild.cs
├── RecordManager.cs
├── RegistryConfig.cs
├── SessionHandler.cs
├── SuperSocket.ClientEngine.Core.0.10.0
└── SuperSocket.ClientEngine.dll
├── TrainingHandler.cs
├── Types.cs
├── UniWebViewManager.cs
├── Utils.cs
├── WebSocket4Net.0.15.2
└── WebSocket4Net.dll
├── WebsocketCortexClient.cs
└── com.cdm.authentication
├── Plugins
└── iOS
│ ├── ASWebAuthenticationSession.mm
│ └── Common.h
├── Runtime
├── Browser
│ ├── ASWebAuthenticationSession.cs
│ ├── ASWebAuthenticationSessionBrowser.cs
│ ├── ASWebAuthenticationSessionError.cs
│ ├── ASWebAuthenticationSessionErrorCode.cs
│ ├── BrowserResult.cs
│ ├── BrowserStatus.cs
│ ├── CallbackManager.cs
│ ├── CrossPlatformBrowser.cs
│ ├── DeepLinkBrowser.cs
│ ├── IBrowser.cs
│ ├── StandaloneBrowser.cs
│ └── WindowsSystemBrowser.cs
├── Cdm.Authentication.asmdef
├── Clients
│ └── MockServerAuth.cs
├── IUserInfo.cs
├── IUserInfoProvider.cs
├── OAuth2
│ ├── AccessTokenRequest.cs
│ ├── AccessTokenRequestError.cs
│ ├── AccessTokenRequestErrorCode.cs
│ ├── AccessTokenRequestException.cs
│ ├── AccessTokenResponse.cs
│ ├── AccessTokenResponseExtensions.cs
│ ├── AuthenticationError.cs
│ ├── AuthenticationException.cs
│ ├── AuthenticationSession.cs
│ ├── AuthorizationCodeFlow.cs
│ ├── AuthorizationCodeFlowWithPkce.cs
│ ├── AuthorizationCodeRequest.cs
│ ├── AuthorizationCodeRequestError.cs
│ ├── AuthorizationCodeRequestErrorCode.cs
│ ├── AuthorizationCodeRequestException.cs
│ ├── AuthorizationCodeResponse.cs
│ └── RefreshTokenRequest.cs
├── Utils
│ ├── JsonHelper.cs
│ └── UrlBuilder.cs
└── csc.rsp
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # This .gitignore file should be placed at the root of your Unity project directory
2 | #
3 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
4 | #
5 | .utmp/
6 | /[Ll]ibrary/
7 | /[Tt]emp/
8 | /[Oo]bj/
9 | /[Bb]uild/
10 | /[Bb]uilds/
11 | /[Ll]ogs/
12 | /[Uu]ser[Ss]ettings/
13 |
14 | # MemoryCaptures can get excessive in size.
15 | # They also could contain extremely sensitive data
16 | /[Mm]emoryCaptures/
17 |
18 | # Recordings can get excessive in size
19 | /[Rr]ecordings/
20 |
21 | # Uncomment this line if you wish to ignore the asset store tools plugin
22 | # /[Aa]ssets/AssetStoreTools*
23 |
24 | # Autogenerated Jetbrains Rider plugin
25 | /[Aa]ssets/Plugins/Editor/JetBrains*
26 |
27 | # Visual Studio cache directory
28 | .vs/
29 |
30 | # Gradle cache directory
31 | .gradle/
32 |
33 | # Autogenerated VS/MD/Consulo solution and project files
34 | ExportedObj/
35 | .consulo/
36 | *.csproj
37 | *.unityproj
38 | *.sln
39 | *.suo
40 | *.tmp
41 | *.user
42 | *.userprefs
43 | *.pidb
44 | *.booproj
45 | *.svd
46 | *.pdb
47 | *.mdb
48 | *.opendb
49 | *.VC.db
50 |
51 | # Unity3D generated meta files
52 | *.pidb.meta
53 | *.pdb.meta
54 | *.mdb.meta
55 | *.meta
56 |
57 | # Unity3D generated file on crash reports
58 | sysinfo.txt
59 |
60 | # Builds
61 | *.apk
62 | *.aab
63 | *.unitypackage
64 | *.unitypackage.meta
65 | *.app
66 |
67 | # Crashlytics generated file
68 | crashlytics-build.properties
69 |
70 | # Packed Addressables
71 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
72 |
73 | # Temporary auto-generated Android Assets
74 | /[Aa]ssets/[Ss]treamingAssets/aa.meta
75 | /[Aa]ssets/[Ss]treamingAssets/aa/*
76 |
77 | # macOS
78 | .DS_Store
79 |
80 | # other
81 | **/*.framework
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "Src/uniwebview"]
2 | path = Src/uniwebview
3 | url = git@github.com:Emotiv/uniwebview.git
4 |
--------------------------------------------------------------------------------
/.jenkins/application/Jenkinsfile:
--------------------------------------------------------------------------------
1 | @Library('shared_jenkins_pipeline@COR-5754') _
2 |
3 | import org.emotiv.unity.UnityBuild
4 |
5 | def app_unity = new UnityBuild(this)
6 |
7 | def get_app_version() {
8 | return processConfig('.jenkins/application/config_release.json')
9 | }
10 |
11 | def get_cortex_job_build(is_build_product) {
12 | def cortex_parent_job = 'Cortex-Lib-Mobile-Unity'
13 | if(!is_build_product)
14 | {
15 | def job_name = 'develop'
16 | return ["${cortex_parent_job}/${job_name}",
17 | "${env.JENKINS_URL}/job/${cortex_parent_job}/job/${job_name}"]
18 | }
19 | else
20 | {
21 | if (env.BRANCH_NAME == 'master') {
22 | def job_name = 'master'
23 | return ["${cortex_parent_job}/${job_name}",
24 | "${env.JENKINS_URL}/job/${cortex_parent_job}/job/${job_name}"]
25 | } else {
26 | def job_name = "${env.BRANCH_NAME}".replace("/", "%2F")
27 | return ["${cortex_parent_job}/${job_name}",
28 | "${env.JENKINS_URL}/job/${cortex_parent_job}/job/${job_name}"]
29 | }
30 | }
31 | }
32 |
33 | def build_unity_plugin(builder, is_build_product = false) {
34 | def cortex_info = get_cortex_job_build(is_build_product)
35 | def cortex_lib_name = null
36 | builder.clean_build_folder()
37 | echo "build with cortex job ${cortex_info[0]}"
38 | if (is_build_product)
39 | {
40 | cortex_lib_name = "EmotivCortexLib-release"
41 | }
42 | else
43 | {
44 | cortex_lib_name = "EmotivCortexLib-debug"
45 | }
46 | builder.copy_cortex_lib_android(cortex_info[0], cortex_info[1], cortex_lib_name)
47 | builder.copy_cortex_lib_ios(cortex_info[0], cortex_info[1])
48 | builder.build_unity_plugin('./Src', 'unity-plugin-package-build')
49 | }
50 |
51 | def clean_build_folder(builder) {
52 | builder.clean_build_folder()
53 | }
54 |
55 | pipeline {
56 | agent none
57 | options {
58 | disableConcurrentBuilds abortPrevious: true
59 | copyArtifactPermission '*'
60 | parallelsAlwaysFailFast()
61 | }
62 | environment {
63 | custom_workspace = "workspace/emotiv-unity-plugin"
64 | UNITY_PATH_MAC = "/Applications/Unity/Hub/Editor/6000.0.36f1/Unity.app/Contents/MacOS/Unity"
65 | }
66 | stages {
67 | stage('Build unity plugin develop version') {
68 | when {
69 | branch 'develop'
70 | }
71 | environment {
72 | build_type = "Debug"
73 | }
74 | agent {
75 | node {
76 | label 'mac_m2'
77 | customWorkspace env.custom_workspace
78 | }
79 | }
80 | steps {
81 | echo 'Building code from branch ' + env.BRANCH_NAME
82 | script {
83 | build_unity_plugin(app_unity)
84 | }
85 | }
86 | post {
87 | success {
88 | archiveArtifacts artifacts: '**/*.unitypackage',
89 | followSymlinks: false,
90 | fingerprint: true
91 | }
92 | cleanup {
93 | clean_build_folder(app_unity)
94 | }
95 | }
96 | }
97 | stage('Build unity plugin master version') {
98 | when {
99 | branch 'master'
100 | }
101 | agent {
102 | node {
103 | label 'mac_m2'
104 | customWorkspace env.custom_workspace
105 | }
106 | }
107 | steps {
108 | echo 'Building code from branch ' + env.BRANCH_NAME
109 | script {
110 | build_unity_plugin(app_unity)
111 | }
112 | }
113 | post {
114 | success {
115 | archiveArtifacts artifacts: '**/*.unitypackage',
116 | followSymlinks: false,
117 | fingerprint: true
118 | }
119 | cleanup {
120 | clean_build_folder(app_unity)
121 | }
122 | }
123 | }
124 | stage('Build unity plugin release version') {
125 | when {
126 | branch 'release/**'
127 | }
128 | agent {
129 | node {
130 | label 'mac_m2'
131 | customWorkspace env.custom_workspace
132 | }
133 | }
134 | steps {
135 | echo 'Building code from branch ' + env.BRANCH_NAME
136 | script {
137 | def app_version = get_app_version()
138 | build_unity_plugin(app_unity)
139 | }
140 | }
141 | post {
142 | success {
143 | archiveArtifacts artifacts: '**/*.unitypackage',
144 | followSymlinks: false,
145 | fingerprint: true
146 | }
147 | cleanup {
148 | clean_build_folder(app_unity)
149 | }
150 | }
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/.jenkins/application/config_release.json:
--------------------------------------------------------------------------------
1 | {
2 | "branch": "release/4.8.8",
3 | "version": "4.8.8"
4 | }
5 |
--------------------------------------------------------------------------------
/Documentation/Images/CodeStructure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Documentation/Images/CodeStructure.png
--------------------------------------------------------------------------------
/Documentation/ReleaseNotes.md:
--------------------------------------------------------------------------------
1 | # Release Notes
2 |
3 | ## Version 4.7 (July 2025)
4 | ### Added
5 | - Support for mobile platforms
6 | - Integration with Emotiv Embedded library
7 |
8 |
9 | ## Version 3.7 (November 2023)
10 | ### Added
11 | - Support new headset refreshing flow where App need to to call ScanHeadsets() to start headset scanning.
12 |
13 | ## Version 2.7 2(10 July 2021)
14 | ### Added
15 | - Support injectMarker and updateMarker to EEG data stream.
16 |
17 | ## Version 2.7 0(17 Apr 2021)
18 |
19 | ### Added
20 | - Support data parsing for new channel BatteryPercent of "dev" stream which is new from Cortex version 2.7.0.
21 |
22 | ### Fixed
23 | - Fixed issue parsing "Markers" channels from eeg data stream. Actually, we exclude "Markers" data from data buffer
24 | - Fixed issue sometime can not add new method \_methodForRequestId map at CortexClient.cs
25 |
26 | ## Version 2.4 (12 May 2020)
27 | For the moment the following features are supported:
28 | - Subscribe to all data streams: EEG, Motion, Device information, Band power, detections, etc.
29 | - Create a record and stop a record
30 | - Create, load and unload profiles
31 | - Perform Mental Commands and Facial Expression training
32 |
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 EMOTIV
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Emotiv Cortex Unity Integration Guide
2 |
3 | This guide will help you work with the Emotiv Cortex API to build your Unity application, supporting both mobile (Android, iOS) and desktop (Windows, macOS) platforms. It covers both available integration options: Emotiv Cortex Service and Emotiv Embedded Library.
4 |
5 | ## Overview
6 |
7 | - **Supported Platforms:**
8 | - Desktop: Windows, macOS
9 | - Mobile: Android, iOS
10 | - **Integration Options:**
11 | 1. **Emotiv Cortex Service** (Desktop only)
12 | 2. **Emotiv Embedded Library** (Mobile and Desktop with `USE_EMBEDDED_LIB` defined)
13 |
14 | ---
15 |
16 | ## Option 1: Emotiv Cortex Service (Desktop Only)
17 |
18 | ### What is it?
19 | Connects your Unity app to the Emotiv Cortex Service running on your desktop. This is the simplest way to get started on Windows and macOS.
20 |
21 | ### Setup Steps
22 | 1. **Install EMOTIV Launcher**
23 | - Download and install the [EMOTIV Launcher](https://www.emotiv.com/products/emotiv-launcher) on your desktop.
24 | 2. **Project Configuration**
25 | - Ensure the scripting symbol `USE_EMBEDDED_LIB` is **not** defined in your Unity project.
26 | - No need to configure or add the Emotiv Embedded library.
27 | 3. **Authentication**
28 | - Login via the EMOTIV Launcher before running your Unity application.
29 |
30 | ---
31 |
32 | ## Option 2: Emotiv Embedded Library (Mobile & Desktop)
33 |
34 | ### What is it?
35 | Integrates the Emotiv Cortex API directly into your Unity app using the Emotiv Embedded Library. This is required for mobile platforms and can also be used on desktop (in development).
36 |
37 | ### Setup Steps
38 | 1. **Contact Emotiv**
39 | - Contact Emotiv to request access to the Emotiv Embedded Library and the private UniWebView submodule: [Contact Emotiv](https://www.emotiv.com/pages/contact).
40 | 2. **Add the Embedded Library**
41 | - **Android:** Place `EmotivCortexLib.aar` in `Src/AndroidPlugin/EmotivCortexLib/`.
42 | - **iOS:** Place `EmotivCortexLib.xcframework` in `Src/IosPlugin/`.
43 | 3. **Add UniWebView Submodule**
44 | - Pull the UniWebView submodule (private repo; access required from Emotiv).
45 | - UniWebView is used to open a webview for authentication on mobile.
46 | 4. **Project Configuration**
47 | - Define the scripting symbol `USE_EMBEDDED_LIB` in your Unity project settings.
48 | - For desktop, support is experimental and still under development.
49 | 5. **Authentication**
50 | - On mobile, authentication is handled in-app via a webview.
51 |
52 | ---
53 |
54 | ## Quick Start: Using `EmotivUnityItf.cs`
55 |
56 | The main interface for your Unity app is the `EmotivUnityItf` class.
57 |
58 | ### Initialization
59 | 1. **Call `Init()`**
60 | - Pass your client ID, client secret, app name, and other optional parameters.
61 | - It is recommended to set `isDataBufferUsing = true` so you can retrieve data from the buffer (see example below).
62 | 2. **Call `Start()`**
63 | - On Android, pass the current activity as a parameter: `Start(currentActivity)`
64 | - On other platforms, you can call `Start()` with no parameters.
65 |
66 | ### Connecting to a Headset
67 | 1. **After authorization is complete**, call `QueryHeadsets()` to discover available headsets.
68 | 2. **Create a session** with a headset using `CreateSessionWithHeadset(headsetId)`.
69 | - This only creates a session. To receive data, you must call `SubscribeData(streamList)` with the desired data streams (e.g., EEG, motion, etc.).
70 | - Alternatively, you can use `StartDataStream(streamList, headsetId)` to create a session and subscribe to data in one step.
71 |
72 | #### Example: Creating a session and subscribing to EEG data
73 | ```csharp
74 | // Create session with headset
75 | EmotivUnityItf.Instance.CreateSessionWithHeadset(headsetId);
76 | // Before subscribing, check if the session is created
77 | if (EmotivUnityItf.Instance.IsSessionCreated)
78 | {
79 | // Subscribe to EEG data
80 | EmotivUnityItf.Instance.SubscribeData(new List { "eeg" });
81 | }
82 | // OR combine both steps:
83 | EmotivUnityItf.Instance.StartDataStream(new List { "eeg" }, headsetId);
84 | ```
85 |
86 | #### Example: Getting EEG data from buffer
87 | ```csharp
88 | // Make sure isDataBufferUsing = true in Init()
89 | int n = EmotivUnityItf.Instance.GetNumberEEGSamples();
90 | if (n > 0)
91 | {
92 | foreach (var chan in EmotivUnityItf.Instance.GetEEGChannels())
93 | {
94 | double[] data = EmotivUnityItf.Instance.GetEEGData(chan);
95 | // process data
96 | }
97 | }
98 | ```
99 |
100 | ---
101 |
102 | ## Recording and Markers
103 |
104 | After creating a session, you can start recording EEG data, inject markers, and export multiple records. Please note that when stopping a recording, wait until you receive `WarningCode = 30` to ensure data processing is finished before exporting the record.
105 |
106 | - Start recording:
107 | ```csharp
108 | EmotivUnityItf.Instance.StartRecord("MyRecordTitle");
109 | ```
110 | - Inject a marker:
111 | ```csharp
112 | EmotivUnityItf.Instance.InjectMarker("EventLabel", "EventValue");
113 | ```
114 | - Stop recording:
115 | ```csharp
116 | EmotivUnityItf.Instance.StopRecord();
117 | ```
118 | - Export multiple records (after data processing is complete):
119 | ```csharp
120 | // Export a record to the desktop
121 | string folderPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Desktop);
122 | List recordsToExport = new List { "recordId" };
123 | List streamTypes = new List { "EEG", "MOTION" }; // Specify the stream types you want to export
124 | string format = "CSV"; // or "CSV", "EDFPLUS", "BDFPLUS"
125 | string version = "V2"; // Optional, specify if needed
126 | EmotivUnityItf.Instance.ExportRecord(recordsToExport, folderPath, streamTypes, format, version);
127 | ```
128 |
129 | ## Profile Management and Training
130 |
131 | To use BCI training, you need to load a profile for the current headset.
132 |
133 | ```csharp
134 | // Load or create and load a profile for the current headset.If the profile does not exist, it will be created and loaded automatically.
135 | EmotivUnityItf.Instance.LoadProfile("ProfileName");
136 | // Start mental command training for an action (e.g., "push")
137 | EmotivUnityItf.Instance.StartMCTraining("push");
138 | ```
139 |
140 | See `EmotivUnityItf.cs` for more training and profile management functions.
141 |
142 | ---
143 |
144 | ## Additional Notes
145 | - For mobile builds, ensure all required permissions (Bluetooth, etc.) are set in your Unity project.
146 | - For Option 2, both the Embedded Library and UniWebView are private and require Emotiv approval for access.
147 | - For the latest updates and troubleshooting, contact Emotiv support.
148 |
149 | ---
150 |
151 | ## Example Code
152 |
153 | ```csharp
154 | // Initialization
155 | EmotivUnityItf.Instance.Init(clientId, clientSecret, appName);
156 |
157 | // Start authorization (Android requires currentActivity)
158 | #if UNITY_ANDROID
159 | EmotivUnityItf.Instance.Start(currentActivity);
160 | #else
161 | EmotivUnityItf.Instance.Start();
162 | #endif
163 |
164 | // After authorization
165 | EmotivUnityItf.Instance.QueryHeadsets();
166 | EmotivUnityItf.Instance.CreateSessionWithHeadset(headsetId);
167 | ```
168 |
169 | ---
170 |
171 | For more details, see the comments in `EmotivUnityItf.cs` or contact Emotiv for support.
172 |
173 | ## Release Notes
174 | See [Documentation/ReleaseNotes.md](Documentation/ReleaseNotes.md).
175 |
176 | ## License
177 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
178 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/EmotivCortexLib/build.gradle:
--------------------------------------------------------------------------------
1 | configurations.maybeCreate("default")
2 | artifacts.add("default", file('EmotivCortexLib.aar'))
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | }
4 |
5 | android {
6 | namespace 'com.emotiv.unityplugin'
7 | compileSdk 34
8 |
9 | defaultConfig {
10 | applicationId "com.emotiv.unityplugin"
11 | minSdk 24
12 | targetSdk 34
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | packagingOptions {
30 | exclude '**/*.meta'
31 | }
32 | }
33 |
34 | dependencies {
35 |
36 | implementation libs.appcompat
37 | implementation libs.material
38 | testImplementation libs.junit
39 | androidTestImplementation libs.ext.junit
40 | androidTestImplementation libs.espresso.core
41 | }
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
16 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | UnityPlugin
3 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | android.enableJetifier=true
19 | # Enables namespacing of each library's R class so that its R class includes only the
20 | # resources declared in the library itself and none from the library's dependencies,
21 | # thereby reducing the size of the R class for that library
22 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/AndroidPlugin/unityplugin/consumer-rules.pro
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/src/main/java/com/emotiv/unityplugin/CortexConnection.java:
--------------------------------------------------------------------------------
1 | package com.emotiv.unityplugin;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.StrictMode;
6 | import android.util.Log;
7 |
8 | import androidx.annotation.NonNull;
9 |
10 | import com.emotiv.CortexClient;
11 | import com.emotiv.ResponseHandler;
12 | import org.json.JSONObject;
13 |
14 | public class CortexConnection implements ResponseHandler {
15 | private final String TAG = CortexConnection.class.getName();
16 | private CortexConnectionInterface mCortexConnectionInterface;
17 | // private int mCurrentStreamID = -1;
18 | private CortexClient mCortexClient = null;
19 |
20 | public CortexConnection() {
21 | // TODO: What is thread policy
22 | StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
23 | StrictMode.setThreadPolicy(policy);
24 | }
25 |
26 | public void setCortexLibConnectionInterface(CortexConnectionInterface cortexConnectionInterface) {
27 | this.mCortexConnectionInterface = cortexConnectionInterface;
28 | }
29 |
30 | public void open() {
31 | mCortexClient = new CortexClient();
32 | mCortexClient.registerResponseHandler(this);
33 | }
34 |
35 | public void close() {
36 | mCortexClient.close();
37 | }
38 |
39 | public void sendRequest(String contentRequest) {
40 | try {
41 | if (mCortexClient != null) {
42 |
43 | mCortexClient.sendRequest(contentRequest);
44 | Log.i(TAG, contentRequest);
45 | }
46 | } catch (Exception e) { e.printStackTrace(); }
47 | }
48 |
49 | public void authenticate(@NonNull Activity activity, String clientId, int activityHandleCode)
50 | {
51 | try {
52 | if (mCortexClient != null) {
53 | mCortexClient.authenticate(activity, clientId, activityHandleCode);
54 | }
55 | } catch (Exception e) { e.printStackTrace(); }
56 |
57 | }
58 |
59 | public String getAuthenticationCode(int requestCode, @NonNull Intent intent)
60 | {
61 | String result = "";
62 | try {
63 | if (mCortexClient != null) {
64 | result = mCortexClient.getAuthenticationCode(requestCode, intent);
65 | }
66 | } catch (Exception e) { e.printStackTrace(); }
67 | return result;
68 | }
69 |
70 | @Override
71 | public void processResponse(String s) {
72 | try {
73 | mCortexConnectionInterface.onReceivedMessage(s);
74 |
75 | // JSONObject jsonObj = new JSONObject(s);
76 | // // received warning message in response to a RPC request
77 | // if (jsonObj.has("warning")) {
78 | // mCortexConnectionInterface.onReceivedWarningMessage(s);
79 | // }
80 | // // error
81 | // else if (jsonObj.has("error")) {
82 | // JSONObject error = jsonObj.getJSONObject("error");
83 | // mCortexConnectionInterface.onError(error);
84 | // }
85 | // // received data from a data stream
86 | // else if (jsonObj.has("sid")) {
87 | // mCortexConnectionInterface.onReceivedNewData(s);
88 | // }
89 | // // received message in response to a RPC request
90 | // else if (jsonObj.has("id")) {
91 | // mCortexConnectionInterface.onReceivedMessage(s, mCurrentStreamID);
92 | // }
93 |
94 | } catch (Exception e) { e.printStackTrace(); }
95 | }
96 | }
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/src/main/java/com/emotiv/unityplugin/CortexConnectionInterface.java:
--------------------------------------------------------------------------------
1 | package com.emotiv.unityplugin;
2 |
3 | import org.json.JSONObject;
4 |
5 | public interface CortexConnectionInterface {
6 | void onReceivedMessage(String msg);
7 | void onCortexStarted();
8 | }
9 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/src/main/java/com/emotiv/unityplugin/CortexLibActivity.java:
--------------------------------------------------------------------------------
1 | package com.emotiv.unityplugin;
2 |
3 | import android.app.Application;
4 | import android.Manifest;
5 | import android.content.pm.PackageManager;
6 | import android.os.Build;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 |
10 | import androidx.annotation.NonNull;
11 | import androidx.appcompat.app.AppCompatActivity;
12 | import androidx.core.app.ActivityCompat;
13 | import androidx.core.content.ContextCompat;
14 | import org.json.JSONObject;
15 |
16 | import com.emotiv.CortexLibInterface;
17 | import com.emotiv.CortexLogLevel;
18 | import com.emotiv.CortexLogHandler;
19 | import android.util.Log;
20 | /**
21 | * This class is the base class for MainActivity when they want to work with EmotivCortexLib.aar
22 | * In this activity, we will request some permissions needed by CortexLib and start/stop CortexLib.
23 | */
24 | public class CortexLibActivity implements CortexLibInterface {
25 | private final String TAG = CortexLibActivity.class.getName();
26 | private boolean mCortexStarted = false;
27 | protected CortexConnection mCortexConnection = null;
28 | protected CortexConnectionInterface mCortexConnectionItf = null;
29 |
30 | protected JavaLogInterface mJavaLogInterface = null;
31 |
32 | private static final CortexLibActivity ourInstance = new CortexLibActivity();
33 |
34 | public static CortexLibActivity getInstance() {
35 | return ourInstance;
36 | }
37 |
38 | public void load(Application application) {
39 | CortexLibManager.load(application);
40 | }
41 |
42 | public void start(CortexConnectionInterface cortexResponseInterface) {
43 | if (mCortexStarted) {
44 | onCortexStarted();
45 | }
46 | else {
47 | mCortexConnectionItf = cortexResponseInterface;
48 | CortexLibManager.setLogHandler(CortexLogLevel.INFO, s -> {
49 | if(mJavaLogInterface != null) {
50 | mJavaLogInterface.onReceivedLog(s);
51 | }
52 | });
53 | CortexLibManager.start(this);
54 | }
55 | }
56 |
57 | public void setJavaLogInterface(JavaLogInterface javaLogInterface) {
58 | mJavaLogInterface = javaLogInterface;
59 | }
60 |
61 | public void stop() {
62 | CortexLibManager.stop();
63 | }
64 |
65 | public void sendRequest(String contentRequest) {
66 | if (mCortexConnection != null) {
67 | mCortexConnection.sendRequest(contentRequest);
68 | }
69 | }
70 |
71 | @Override
72 | public void onCortexStarted() {
73 | mCortexStarted = true;
74 | mCortexConnection = CortexLibManager.createConnection(mCortexConnectionItf);
75 | mCortexConnectionItf.onCortexStarted();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/src/main/java/com/emotiv/unityplugin/CortexLibManager.java:
--------------------------------------------------------------------------------
1 | package com.emotiv.unityplugin;
2 |
3 | import android.app.Application;
4 | import com.emotiv.CortexLibInterface;
5 | import com.emotiv.CortexLogHandler;
6 | import com.emotiv.CortexLogLevel;
7 | import com.emotiv.EmotivLibraryLoader;
8 | import com.emotiv.CortexLib;
9 | import android.util.Log;
10 |
11 | public class CortexLibManager {
12 | private final String TAG = CortexLibManager.class.getName();
13 |
14 | // This method should be called before start(), stop()
15 | public static void load(Application application) {
16 | Log.i("CortexLibManager", "load start: ");
17 | EmotivLibraryLoader loader = new EmotivLibraryLoader(application);
18 | loader.load();
19 | }
20 |
21 | public static void testLoad(int a) {
22 | Log.i("CortexLibManager", "testLoad " + Integer.toString(a));
23 | }
24 |
25 | // This method should be called after load() and before start()
26 | public static void setLogHandler(CortexLogLevel logLevel, CortexLogHandler logHandler) {
27 | CortexLib.setLogHandler(logLevel, logHandler);
28 | }
29 |
30 | // CortexLib requires some permissions and
31 | // this method should be called after users granted permissions to the app
32 | public static boolean start(CortexLibInterface cortexLibInterface) {
33 | // start the CortexLib
34 | return CortexLib.start(cortexLibInterface);
35 | }
36 |
37 | // This method should be called before the app is about to quit
38 | public static void stop() {
39 | CortexLib.stop();
40 | }
41 |
42 | // This method should be called after CortexLibInterface.onCortexStarted() callback is triggered
43 | public static CortexConnection createConnection(CortexConnectionInterface cortexConnectionInterface) {
44 | CortexConnection connection = new CortexConnection();
45 | connection.setCortexLibConnectionInterface(cortexConnectionInterface);
46 | connection.open();
47 | return connection;
48 | }
49 |
50 | // This method should be called before stop()
51 | public static void closeConnection(CortexConnection connection) {
52 | connection.close();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Src/AndroidPlugin/unityplugin/src/main/java/com/emotiv/unityplugin/JavaLogInterface.java:
--------------------------------------------------------------------------------
1 | package com.emotiv.unityplugin;
2 |
3 | public interface JavaLogInterface {
4 | void onReceivedLog(String msg);
5 | }
6 |
--------------------------------------------------------------------------------
/Src/BandPowerDataBuffer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections;
4 | using EmotivUnityPlugin;
5 | using Newtonsoft.Json.Linq;
6 |
7 |
8 |
9 | ///
10 | /// Band power data buffer.
11 | ///
12 | public class BandPowerDataBuffer : DataBuffer
13 | {
14 | BufferStream[] bufHi; // high rate buffer
15 |
16 | List _bandPowerList = new List();
17 |
18 | public static Dictionary BandPowerMap = new Dictionary() {
19 | {BandPowerType.Thetal, "theta"},
20 | {BandPowerType.Alpha, "alpha"},
21 | {BandPowerType.BetalL, "betaL"},
22 | {BandPowerType.BetalH, "betaH"},
23 | {BandPowerType.Gamma, "gamma"}
24 | };
25 |
26 | public List BandPowerList { get => _bandPowerList; set => _bandPowerList = value; }
27 |
28 | public void SetChannels(JArray bandPowerLists)
29 | {
30 | string timestamp = ChannelStringList.ChannelToString(Channel_t.CHAN_TIME_SYSTEM);
31 | _bandPowerList.Add(timestamp);
32 | foreach(var item in bandPowerLists){
33 | string chanStr = item.ToString();
34 | _bandPowerList.Add(chanStr);
35 | }
36 | }
37 |
38 | public void Clear() {
39 |
40 | if (bufHi != null) {
41 | Array.Clear(bufHi, 0, bufHi.Length);
42 | bufHi = null;
43 | }
44 | }
45 |
46 | public override void SettingBuffer(int winSize, int step, int headerCount) {
47 | int buffSize = headerCount + 1; // include "TIMESTAMP"
48 | bufHi = new BufferStream[buffSize];
49 | UnityEngine.Debug.Log("POW Setting Buffer size" + bufHi.Length);
50 | for (int i = 0; i < buffSize; i++)
51 | {
52 | if (bufHi[i] == null){
53 | bufHi[i] = new BufferStream(winSize, step);
54 | }
55 | else {
56 | bufHi[i].Reset();
57 | bufHi[i].WindowSize = winSize;
58 | bufHi[i].StepSize = step;
59 | }
60 | }
61 | }
62 |
63 | // event handler
64 | public void OnBandPowerReceived(object sender, ArrayList data)
65 | {
66 | // UnityEngine.Debug.Log("OnBandPowerReceived " + data[2].ToString() + " at " + Utils.GetEpochTimeNow().ToString());
67 | AddDataToBuffer(data);
68 | }
69 |
70 | public override void AddDataToBuffer(ArrayList data)
71 | {
72 | for (int i=0 ; i < data.Count; i++) {
73 | if (data[i] != null) {
74 | double powerData = Convert.ToDouble(data[i]);
75 | bufHi[i].AppendData(powerData);
76 | }
77 | }
78 | }
79 | public override double[] GetDataFromBuffer(int index)
80 | {
81 | return bufHi[index].NextWithRemoval();
82 | }
83 | public override double[] GetLatestDataFromBuffer(int index)
84 | {
85 | double[] nextSegment = null;
86 | double[] lastSegment = null;
87 | do
88 | {
89 | lastSegment = nextSegment;
90 | nextSegment = GetDataFromBuffer(index);
91 | }
92 | while (nextSegment != null);
93 | return lastSegment;
94 | }
95 |
96 | public double GamaPower(Channel_t channel)
97 | {
98 | if (channel == Channel_t.CHAN_FLEX_CMS || channel == Channel_t.CHAN_FLEX_DRL)
99 | return 0;
100 |
101 | double[] chanData = GetLatestDataFromBuffer(GetPowerIndex(channel, BandPowerType.Gamma));
102 | if (chanData != null) {
103 | return chanData[0];
104 | } else {
105 | return 0;
106 | }
107 | }
108 | public double ThetalPower(Channel_t channel)
109 | {
110 | if (channel == Channel_t.CHAN_FLEX_CMS || channel == Channel_t.CHAN_FLEX_DRL)
111 | return 0;
112 |
113 | double[] chanData = GetLatestDataFromBuffer(GetPowerIndex(channel, BandPowerType.Thetal));
114 | if (chanData != null) {
115 | return chanData[0];
116 | }
117 | else {
118 | return 0;
119 | }
120 |
121 | }
122 | public double AlphaPower(Channel_t channel)
123 | {
124 | if (channel == Channel_t.CHAN_FLEX_CMS || channel == Channel_t.CHAN_FLEX_DRL)
125 | return 0;
126 |
127 | try
128 | {
129 | double[] chanData = GetLatestDataFromBuffer(GetPowerIndex(channel, BandPowerType.Alpha));
130 | if (chanData != null) {
131 | return chanData[0];
132 | } else {
133 | return 0;
134 | }
135 | }
136 | catch (System.Exception e)
137 | {
138 | UnityEngine.Debug.Log("Exception :" + e.Message + " chan " + channel + " index " + GetPowerIndex(channel, BandPowerType.Alpha));
139 | return 0;
140 | }
141 | }
142 |
143 | public double BetalLPower(Channel_t channel)
144 | {
145 | if (channel == Channel_t.CHAN_FLEX_CMS || channel == Channel_t.CHAN_FLEX_DRL)
146 | return 0;
147 |
148 | double[] chanData = GetLatestDataFromBuffer(GetPowerIndex(channel, BandPowerType.BetalL));
149 | if (chanData != null) {
150 | return chanData[0];
151 | } else {
152 | return 0;
153 | }
154 | }
155 | public double BetalHPower(Channel_t channel)
156 | {
157 | if (channel == Channel_t.CHAN_FLEX_CMS || channel == Channel_t.CHAN_FLEX_DRL) {
158 | return 0;
159 | }
160 |
161 | double[] chanData = GetLatestDataFromBuffer(GetPowerIndex(channel, BandPowerType.BetalH));
162 | if (chanData != null) {
163 | return chanData[0];
164 | } else {
165 | return 0;
166 | }
167 | }
168 |
169 | public void PrintPower(BandPowerType powerType){
170 |
171 | // TODO: Check correct data
172 | double power = 0;
173 | if (powerType == BandPowerType.Alpha){
174 | power = AlphaPower(Channel_t.CHAN_AF3);
175 | }
176 | else if (powerType == BandPowerType.Gamma) {
177 | power = GamaPower(Channel_t.CHAN_AF3);
178 | }
179 | UnityEngine.Debug.Log("======PrintPower: type" + (int)powerType + " AF3: "+ power.ToString());
180 | }
181 |
182 | public int GetPowerIndex(Channel_t channel, BandPowerType powerType) {
183 | if (channel == Channel_t.CHAN_TIME_SYSTEM)
184 | return 0;
185 | string chanStr = ChannelStringList.ChannelToString(channel);
186 | string powStr = BandPowerMap[powerType];
187 | int chanIndex = _bandPowerList.IndexOf(chanStr+ "/" + powStr); // TODO check chanIndex = -1
188 | return chanIndex;
189 | }
190 |
191 | public int GetBufferSize()
192 | {
193 | if(bufHi[2] == null)
194 | return 0;
195 |
196 | return bufHi[2].GetBufSize(); // get buffer size of "AF3/alpha"
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/Src/BufferStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections;
4 |
5 | public class BufferStream
6 | {
7 | public ArrayList _buf = null;
8 | int _winSize = 0;
9 | int _stepSize = 0;
10 | int _curPos = 0;
11 | const int BUFFER_SIZE = 1024;
12 |
13 | static readonly object _object = new object();
14 |
15 | public BufferStream(int windowSize, int stepSize)
16 | {
17 | _buf = new ArrayList();
18 | _winSize = windowSize;
19 | _stepSize = stepSize;
20 | _curPos = 0;
21 | }
22 |
23 | public int WindowSize
24 | {
25 | get {
26 | return _winSize;
27 | }
28 | set {
29 | _winSize = value;
30 | }
31 | }
32 |
33 | public int StepSize
34 | {
35 | get {
36 | return _stepSize;
37 | }
38 | set {
39 | _stepSize = value;
40 | }
41 | }
42 |
43 | void processOverFlow()
44 | {
45 | if (_buf.Count <= 0)
46 | return;
47 |
48 | int overflow = _buf.Count - BUFFER_SIZE;
49 | if (overflow > 0) {
50 | _buf.RemoveRange(0, overflow);
51 | _curPos -= overflow;
52 | if(_curPos < 0)
53 | _curPos = 0;
54 | }
55 | }
56 |
57 | public void AppendData(double data)
58 | {
59 | lock (_object)
60 | {
61 | _buf.Add(data);
62 | processOverFlow();
63 | }
64 | }
65 |
66 | public void AppendData(double[] data)
67 | {
68 | lock (_object)
69 | {
70 | _buf.AddRange(data);
71 | processOverFlow();
72 | }
73 | }
74 |
75 | // Get the next segment base on window size and step size
76 | // Return a newly allocated array
77 | private double[] Next()
78 | {
79 | if (_buf.Count == 0)
80 | return null;
81 |
82 | // Not enough data for next segment, return
83 | if (_curPos + _winSize > _buf.Count)
84 | return null;
85 |
86 | // Construct segment from buffer
87 | double[] segment = new double[_winSize];
88 |
89 | _buf.CopyTo(_curPos, segment, 0, _winSize);
90 |
91 | // Increment current position by step size
92 | _curPos += _stepSize;
93 |
94 | // Return segment
95 | return segment;
96 | }
97 |
98 | public double[] NextWithRemoval()
99 | {
100 | lock (_object)
101 | {
102 | double[] segment = Next();
103 |
104 | if (segment == null)
105 | return segment;
106 |
107 | if (_buf.Count == _stepSize) {
108 | _buf = null;
109 | _buf = new ArrayList();
110 | }
111 | else {
112 | _buf.RemoveRange(0, _curPos);
113 | }
114 |
115 | ResetCurrentPosition();
116 | return segment;
117 | }
118 | }
119 |
120 | private void ResetCurrentPosition()
121 | {
122 | _curPos = 0;
123 | }
124 |
125 | public void Reset()
126 | {
127 | lock (_object)
128 | {
129 | _buf = null;
130 | _buf = new ArrayList();
131 | ResetCurrentPosition();
132 | }
133 | }
134 |
135 | public void ViewBuffer()
136 | {
137 | int i = 0;
138 | double ss = 0;
139 | string s = string.Empty;
140 | while (0==0)
141 | {
142 | try
143 | {
144 | ss += (double)_buf[i];
145 | s += _buf[i].ToString().Substring(0, 4) + " ";
146 | i++;
147 | }
148 | catch (System.ArgumentOutOfRangeException)
149 | {
150 | break;
151 | }
152 | }
153 | ss /= i;
154 | UnityEngine.Debug.Log(s);
155 | UnityEngine.Debug.Log(ss);
156 | }
157 |
158 | public int GetBufSize()
159 | {
160 | lock (_object)
161 | {
162 | return _buf.Count;
163 | }
164 | }
165 | }
166 |
167 |
--------------------------------------------------------------------------------
/Src/Config.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace EmotivUnityPlugin
4 | {
5 | ///
6 | /// Contain common Config of a Unity App.
7 | ///
8 | public static class Config
9 | {
10 | public static string AppClientId = "";
11 | public static string AppClientSecret = "";
12 | public static string AppUrl = "";
13 | public static string AppName = "";
14 | public static string ProviderName = "";
15 | public static string LogDirectory = "";
16 | public static string DataDirectory = "";
17 |
18 | public static string EmotivAppsPath = ""; // location of emotiv Apps . Eg: C:\Program Files\EmotivApps
19 | public static string TmpDataFileName = "data.dat";
20 | public static string ProfilesDir = "Profiles";
21 | public static string LogsDir = "UnityLogs";
22 | public static int QUERY_HEADSET_TIME = 1000;
23 | public static int TIME_CLOSE_STREAMS = 1000;
24 | public static int RETRY_CORTEXSERVICE_TIME = 5000;
25 | public static int WAIT_USERLOGIN_TIME = 5000;
26 |
27 | // If you use an Epoc Flex headset, then you must put your configuration here
28 | // TODO: need detail here
29 | public static string FlexMapping = @"{
30 | 'CMS':'TP8', 'DRL':'P6',
31 | 'RM':'TP10','RN':'P4','RO':'P8'}";
32 |
33 | public static void Init(
34 | string clientId,
35 | string clientSecret,
36 | string appName,
37 | bool allowSaveLogAndDataToFile,
38 | string appUrl,
39 | string providerName,
40 | string emotivAppsPath
41 | )
42 | {
43 | AppClientId = clientId;
44 | AppClientSecret = clientSecret;
45 | AppName = appName;
46 | AppUrl = appUrl;
47 | ProviderName = providerName;
48 | EmotivAppsPath = emotivAppsPath;
49 |
50 | if (allowSaveLogAndDataToFile)
51 | {
52 | // create tmp directory for unity app
53 | string tmpPath = Utils.GetAppTmpPath(providerName, appName);
54 | LogDirectory = Path.Combine(tmpPath, LogsDir);
55 | DataDirectory = Path.Combine(tmpPath, ProfilesDir);
56 |
57 | // Ensure the directories exist
58 | if (!Directory.Exists(LogDirectory))
59 | {
60 | Directory.CreateDirectory(LogDirectory);
61 | }
62 |
63 | if (!Directory.Exists(DataDirectory))
64 | {
65 | Directory.CreateDirectory(DataDirectory);
66 | }
67 | }
68 | else
69 | {
70 | LogDirectory = "";
71 | DataDirectory = "";
72 | }
73 |
74 | }
75 | }
76 |
77 | public static class DataStreamName
78 | {
79 | public const string DevInfos = "dev";
80 | public const string EEG = "eeg";
81 | public const string Motion = "mot";
82 | public const string PerformanceMetrics = "met";
83 | public const string BandPower = "pow";
84 | public const string MentalCommands = "com";
85 | public const string FacialExpressions = "fac";
86 | public const string SysEvents = "sys"; // System events of the mental commands and facial expressions
87 | public const string EQ = "eq"; // EEG quality
88 | }
89 |
90 | public static class WarningCode
91 | {
92 | public const int StreamStop = 0;
93 | public const int SessionAutoClosed = 1;
94 | public const int UserLogin = 2;
95 | public const int UserLogout = 3;
96 | public const int ExtenderExportSuccess = 4;
97 | public const int ExtenderExportFailed = 5;
98 | public const int UserNotAcceptLicense = 6;
99 | public const int UserNotHaveAccessRight = 7;
100 | public const int UserRequestAccessRight = 8;
101 | public const int AccessRightGranted = 9;
102 | public const int AccessRightRejected = 10;
103 | public const int CannotDetectOSUSerInfo = 11;
104 | public const int CannotDetectOSUSername = 12;
105 | public const int ProfileLoaded = 13;
106 | public const int ProfileUnloaded = 14;
107 | public const int CortexAutoUnloadProfile = 15;
108 | public const int UserLoginOnAnotherOsUser = 16;
109 | public const int EULAAccepted = 17;
110 | public const int StreamWritingClosed = 18;
111 | public const int CortexIsReady = 23;
112 | public const int UserNotAcceptPrivateEULA = 28;
113 | public const int DataPostProcessingFinished = 30; // Data post processing finished, this event is used to notify the app that the data has been processed and is ready for use, for example exporting
114 | public const int HeadsetWrongInformation = 100;
115 | public const int HeadsetCannotConnected = 101;
116 | public const int HeadsetConnectingTimeout = 102;
117 | public const int HeadsetDataTimeOut = 103;
118 | public const int HeadsetConnected = 104;
119 | public const int BTLEPermissionNotGranted = 31;
120 | public const int HeadsetScanFinished = 142;
121 | }
122 |
123 | // error code
124 | public static class ErrorCode {
125 | public const int LoginTokenError = -32108;
126 | public const int AuthorizeTokenError = -32109;
127 | public const int CloudTokenIsRefreshing = -32130;
128 | public const int NotReAuthorizedError = -32170;
129 | public const int CortexTokenCompareErrorAppInfo = -32135;
130 | public const int CortexTokenNotFit = -32034;
131 |
132 | }
133 |
134 | public static class DevStreamParams
135 | {
136 | public const string battery = "Battery";
137 | public const string signal = "Signal";
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Src/CortexApi/CortexLib.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | //
4 | // This file was automatically generated by SWIG (https://www.swig.org).
5 | // Version 4.2.1
6 | //
7 | // Do not make changes to this file unless you know what you are doing - modify
8 | // the SWIG interface file instead.
9 | //------------------------------------------------------------------------------
10 |
11 |
12 | public class CortexLib : global::System.IDisposable {
13 | private global::System.Runtime.InteropServices.HandleRef swigCPtr;
14 | protected bool swigCMemOwn;
15 |
16 | internal CortexLib(global::System.IntPtr cPtr, bool cMemoryOwn) {
17 | swigCMemOwn = cMemoryOwn;
18 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr);
19 | }
20 |
21 | internal static global::System.Runtime.InteropServices.HandleRef getCPtr(CortexLib obj) {
22 | return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr;
23 | }
24 |
25 | internal static global::System.Runtime.InteropServices.HandleRef swigRelease(CortexLib obj) {
26 | if (obj != null) {
27 | if (!obj.swigCMemOwn)
28 | throw new global::System.ApplicationException("Cannot release ownership as memory is not owned");
29 | global::System.Runtime.InteropServices.HandleRef ptr = obj.swigCPtr;
30 | obj.swigCMemOwn = false;
31 | obj.Dispose();
32 | return ptr;
33 | } else {
34 | return new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
35 | }
36 | }
37 |
38 | ~CortexLib() {
39 | Dispose(false);
40 | }
41 |
42 | public void Dispose() {
43 | Dispose(true);
44 | global::System.GC.SuppressFinalize(this);
45 | }
46 |
47 | protected virtual void Dispose(bool disposing) {
48 | lock(this) {
49 | if (swigCPtr.Handle != global::System.IntPtr.Zero) {
50 | if (swigCMemOwn) {
51 | swigCMemOwn = false;
52 | EmotivCortexLibPINVOKE.delete_CortexLib(swigCPtr);
53 | }
54 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
55 | }
56 | }
57 | }
58 |
59 | public static void start(CortexStartedEventHandler handler) {
60 | EmotivCortexLibPINVOKE.CortexLib_start(CortexStartedEventHandler.getCPtr(handler));
61 | }
62 |
63 | public static void stop() {
64 | EmotivCortexLibPINVOKE.CortexLib_stop();
65 | }
66 |
67 | public static void setLogHandler(int logLevel, CortexLogEventHandler handler) {
68 | EmotivCortexLibPINVOKE.CortexLib_setLogHandler(logLevel, CortexLogEventHandler.getCPtr(handler));
69 | }
70 |
71 | public CortexLib() : this(EmotivCortexLibPINVOKE.new_CortexLib(), true) {
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/Src/CortexApi/CortexLogEventHandler.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | //
4 | // This file was automatically generated by SWIG (https://www.swig.org).
5 | // Version 4.2.1
6 | //
7 | // Do not make changes to this file unless you know what you are doing - modify
8 | // the SWIG interface file instead.
9 | //------------------------------------------------------------------------------
10 |
11 |
12 | public class CortexLogEventHandler : global::System.IDisposable {
13 | private global::System.Runtime.InteropServices.HandleRef swigCPtr;
14 | protected bool swigCMemOwn;
15 |
16 | internal CortexLogEventHandler(global::System.IntPtr cPtr, bool cMemoryOwn) {
17 | swigCMemOwn = cMemoryOwn;
18 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr);
19 | }
20 |
21 | internal static global::System.Runtime.InteropServices.HandleRef getCPtr(CortexLogEventHandler obj) {
22 | return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr;
23 | }
24 |
25 | internal static global::System.Runtime.InteropServices.HandleRef swigRelease(CortexLogEventHandler obj) {
26 | if (obj != null) {
27 | if (!obj.swigCMemOwn)
28 | throw new global::System.ApplicationException("Cannot release ownership as memory is not owned");
29 | global::System.Runtime.InteropServices.HandleRef ptr = obj.swigCPtr;
30 | obj.swigCMemOwn = false;
31 | obj.Dispose();
32 | return ptr;
33 | } else {
34 | return new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
35 | }
36 | }
37 |
38 | ~CortexLogEventHandler() {
39 | Dispose(false);
40 | }
41 |
42 | public void Dispose() {
43 | Dispose(true);
44 | global::System.GC.SuppressFinalize(this);
45 | }
46 |
47 | protected virtual void Dispose(bool disposing) {
48 | lock(this) {
49 | if (swigCPtr.Handle != global::System.IntPtr.Zero) {
50 | if (swigCMemOwn) {
51 | swigCMemOwn = false;
52 | EmotivCortexLibPINVOKE.delete_CortexLogEventHandler(swigCPtr);
53 | }
54 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
55 | }
56 | }
57 | }
58 |
59 | public virtual void onLogMessage(string message) {
60 | EmotivCortexLibPINVOKE.CortexLogEventHandler_onLogMessage(swigCPtr, message);
61 | if (EmotivCortexLibPINVOKE.SWIGPendingException.Pending) throw EmotivCortexLibPINVOKE.SWIGPendingException.Retrieve();
62 | }
63 |
64 | public CortexLogEventHandler() : this(EmotivCortexLibPINVOKE.new_CortexLogEventHandler(), true) {
65 | SwigDirectorConnect();
66 | }
67 |
68 | private void SwigDirectorConnect() {
69 | if (SwigDerivedClassHasMethod("onLogMessage", swigMethodTypes0))
70 | swigDelegate0 = new SwigDelegateCortexLogEventHandler_0(SwigDirectorMethodonLogMessage);
71 | EmotivCortexLibPINVOKE.CortexLogEventHandler_director_connect(swigCPtr, swigDelegate0);
72 | }
73 |
74 | private bool SwigDerivedClassHasMethod(string methodName, global::System.Type[] methodTypes) {
75 | global::System.Reflection.MethodInfo[] methodInfos = this.GetType().GetMethods(
76 | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance);
77 | foreach (global::System.Reflection.MethodInfo methodInfo in methodInfos) {
78 | if (methodInfo.DeclaringType == null)
79 | continue;
80 |
81 | if (methodInfo.Name != methodName)
82 | continue;
83 |
84 | var parameters = methodInfo.GetParameters();
85 | if (parameters.Length != methodTypes.Length)
86 | continue;
87 |
88 | bool parametersMatch = true;
89 | for (var i = 0; i < parameters.Length; i++) {
90 | if (parameters[i].ParameterType != methodTypes[i]) {
91 | parametersMatch = false;
92 | break;
93 | }
94 | }
95 |
96 | if (!parametersMatch)
97 | continue;
98 |
99 | if (methodInfo.IsVirtual && (methodInfo.DeclaringType.IsSubclassOf(typeof(CortexLogEventHandler))) &&
100 | methodInfo.DeclaringType != methodInfo.GetBaseDefinition().DeclaringType) {
101 | return true;
102 | }
103 | }
104 |
105 | return false;
106 | }
107 |
108 | private void SwigDirectorMethodonLogMessage(string message) {
109 | onLogMessage(message);
110 | }
111 |
112 | public delegate void SwigDelegateCortexLogEventHandler_0(string message);
113 |
114 | private SwigDelegateCortexLogEventHandler_0 swigDelegate0;
115 |
116 | private static global::System.Type[] swigMethodTypes0 = new global::System.Type[] { typeof(string) };
117 | }
118 |
--------------------------------------------------------------------------------
/Src/CortexApi/CortexStartedEventHandler.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | //
4 | // This file was automatically generated by SWIG (https://www.swig.org).
5 | // Version 4.2.1
6 | //
7 | // Do not make changes to this file unless you know what you are doing - modify
8 | // the SWIG interface file instead.
9 | //------------------------------------------------------------------------------
10 |
11 |
12 | public class CortexStartedEventHandler : global::System.IDisposable {
13 | private global::System.Runtime.InteropServices.HandleRef swigCPtr;
14 | protected bool swigCMemOwn;
15 |
16 | internal CortexStartedEventHandler(global::System.IntPtr cPtr, bool cMemoryOwn) {
17 | swigCMemOwn = cMemoryOwn;
18 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr);
19 | }
20 |
21 | internal static global::System.Runtime.InteropServices.HandleRef getCPtr(CortexStartedEventHandler obj) {
22 | return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr;
23 | }
24 |
25 | internal static global::System.Runtime.InteropServices.HandleRef swigRelease(CortexStartedEventHandler obj) {
26 | if (obj != null) {
27 | if (!obj.swigCMemOwn)
28 | throw new global::System.ApplicationException("Cannot release ownership as memory is not owned");
29 | global::System.Runtime.InteropServices.HandleRef ptr = obj.swigCPtr;
30 | obj.swigCMemOwn = false;
31 | obj.Dispose();
32 | return ptr;
33 | } else {
34 | return new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
35 | }
36 | }
37 |
38 | ~CortexStartedEventHandler() {
39 | Dispose(false);
40 | }
41 |
42 | public void Dispose() {
43 | Dispose(true);
44 | global::System.GC.SuppressFinalize(this);
45 | }
46 |
47 | protected virtual void Dispose(bool disposing) {
48 | lock(this) {
49 | if (swigCPtr.Handle != global::System.IntPtr.Zero) {
50 | if (swigCMemOwn) {
51 | swigCMemOwn = false;
52 | EmotivCortexLibPINVOKE.delete_CortexStartedEventHandler(swigCPtr);
53 | }
54 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
55 | }
56 | }
57 | }
58 |
59 | public virtual void onCortexStarted() {
60 | EmotivCortexLibPINVOKE.CortexStartedEventHandler_onCortexStarted(swigCPtr);
61 | }
62 |
63 | public CortexStartedEventHandler() : this(EmotivCortexLibPINVOKE.new_CortexStartedEventHandler(), true) {
64 | SwigDirectorConnect();
65 | }
66 |
67 | private void SwigDirectorConnect() {
68 | if (SwigDerivedClassHasMethod("onCortexStarted", swigMethodTypes0))
69 | swigDelegate0 = new SwigDelegateCortexStartedEventHandler_0(SwigDirectorMethodonCortexStarted);
70 | EmotivCortexLibPINVOKE.CortexStartedEventHandler_director_connect(swigCPtr, swigDelegate0);
71 | }
72 |
73 | private bool SwigDerivedClassHasMethod(string methodName, global::System.Type[] methodTypes) {
74 | global::System.Reflection.MethodInfo[] methodInfos = this.GetType().GetMethods(
75 | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance);
76 | foreach (global::System.Reflection.MethodInfo methodInfo in methodInfos) {
77 | if (methodInfo.DeclaringType == null)
78 | continue;
79 |
80 | if (methodInfo.Name != methodName)
81 | continue;
82 |
83 | var parameters = methodInfo.GetParameters();
84 | if (parameters.Length != methodTypes.Length)
85 | continue;
86 |
87 | bool parametersMatch = true;
88 | for (var i = 0; i < parameters.Length; i++) {
89 | if (parameters[i].ParameterType != methodTypes[i]) {
90 | parametersMatch = false;
91 | break;
92 | }
93 | }
94 |
95 | if (!parametersMatch)
96 | continue;
97 |
98 | if (methodInfo.IsVirtual && (methodInfo.DeclaringType.IsSubclassOf(typeof(CortexStartedEventHandler))) &&
99 | methodInfo.DeclaringType != methodInfo.GetBaseDefinition().DeclaringType) {
100 | return true;
101 | }
102 | }
103 |
104 | return false;
105 | }
106 |
107 | private void SwigDirectorMethodonCortexStarted() {
108 | onCortexStarted();
109 | }
110 |
111 | public delegate void SwigDelegateCortexStartedEventHandler_0();
112 |
113 | private SwigDelegateCortexStartedEventHandler_0 swigDelegate0;
114 |
115 | private static global::System.Type[] swigMethodTypes0 = new global::System.Type[] { };
116 | }
117 |
--------------------------------------------------------------------------------
/Src/CortexApi/EmbeddedCortexClientNative.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | //
4 | // This file was automatically generated by SWIG (https://www.swig.org).
5 | // Version 4.2.1
6 | //
7 | // Do not make changes to this file unless you know what you are doing - modify
8 | // the SWIG interface file instead.
9 | //------------------------------------------------------------------------------
10 |
11 |
12 | public class EmbeddedCortexClientNative : global::System.IDisposable {
13 | private global::System.Runtime.InteropServices.HandleRef swigCPtr;
14 | protected bool swigCMemOwn;
15 |
16 | internal EmbeddedCortexClientNative(global::System.IntPtr cPtr, bool cMemoryOwn) {
17 | swigCMemOwn = cMemoryOwn;
18 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr);
19 | }
20 |
21 | internal static global::System.Runtime.InteropServices.HandleRef getCPtr(EmbeddedCortexClientNative obj) {
22 | return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr;
23 | }
24 |
25 | internal static global::System.Runtime.InteropServices.HandleRef swigRelease(EmbeddedCortexClientNative obj) {
26 | if (obj != null) {
27 | if (!obj.swigCMemOwn)
28 | throw new global::System.ApplicationException("Cannot release ownership as memory is not owned");
29 | global::System.Runtime.InteropServices.HandleRef ptr = obj.swigCPtr;
30 | obj.swigCMemOwn = false;
31 | obj.Dispose();
32 | return ptr;
33 | } else {
34 | return new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
35 | }
36 | }
37 |
38 | ~EmbeddedCortexClientNative() {
39 | Dispose(false);
40 | }
41 |
42 | public void Dispose() {
43 | Dispose(true);
44 | global::System.GC.SuppressFinalize(this);
45 | }
46 |
47 | protected virtual void Dispose(bool disposing) {
48 | lock(this) {
49 | if (swigCPtr.Handle != global::System.IntPtr.Zero) {
50 | if (swigCMemOwn) {
51 | swigCMemOwn = false;
52 | EmotivCortexLibPINVOKE.delete_CortexClient(swigCPtr);
53 | }
54 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
55 | }
56 | }
57 | }
58 |
59 | public EmbeddedCortexClientNative() : this(EmotivCortexLibPINVOKE.new_CortexClient(), true) {
60 | }
61 |
62 | public void sendRequest(string message) {
63 | EmotivCortexLibPINVOKE.CortexClient_sendRequest(swigCPtr, message);
64 | if (EmotivCortexLibPINVOKE.SWIGPendingException.Pending) throw EmotivCortexLibPINVOKE.SWIGPendingException.Retrieve();
65 | }
66 |
67 | public void registerResponseHandler(ResponseHandlerCpp handler) {
68 | EmotivCortexLibPINVOKE.CortexClient_registerResponseHandler(swigCPtr, ResponseHandlerCpp.getCPtr(handler));
69 | }
70 |
71 | public void close() {
72 | EmotivCortexLibPINVOKE.CortexClient_close(swigCPtr);
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Src/CortexApi/EmotivCortexLib.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | //
4 | // This file was automatically generated by SWIG (https://www.swig.org).
5 | // Version 4.2.1
6 | //
7 | // Do not make changes to this file unless you know what you are doing - modify
8 | // the SWIG interface file instead.
9 | //------------------------------------------------------------------------------
10 |
11 |
12 | public class EmotivCortexLib {
13 | }
14 |
--------------------------------------------------------------------------------
/Src/CortexApi/ResponseHandlerCpp.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | //
4 | // This file was automatically generated by SWIG (https://www.swig.org).
5 | // Version 4.2.1
6 | //
7 | // Do not make changes to this file unless you know what you are doing - modify
8 | // the SWIG interface file instead.
9 | //------------------------------------------------------------------------------
10 |
11 |
12 | public class ResponseHandlerCpp : global::System.IDisposable {
13 | private global::System.Runtime.InteropServices.HandleRef swigCPtr;
14 | protected bool swigCMemOwn;
15 |
16 | internal ResponseHandlerCpp(global::System.IntPtr cPtr, bool cMemoryOwn) {
17 | swigCMemOwn = cMemoryOwn;
18 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(this, cPtr);
19 | }
20 |
21 | internal static global::System.Runtime.InteropServices.HandleRef getCPtr(ResponseHandlerCpp obj) {
22 | return (obj == null) ? new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero) : obj.swigCPtr;
23 | }
24 |
25 | internal static global::System.Runtime.InteropServices.HandleRef swigRelease(ResponseHandlerCpp obj) {
26 | if (obj != null) {
27 | if (!obj.swigCMemOwn)
28 | throw new global::System.ApplicationException("Cannot release ownership as memory is not owned");
29 | global::System.Runtime.InteropServices.HandleRef ptr = obj.swigCPtr;
30 | obj.swigCMemOwn = false;
31 | obj.Dispose();
32 | return ptr;
33 | } else {
34 | return new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
35 | }
36 | }
37 |
38 | ~ResponseHandlerCpp() {
39 | Dispose(false);
40 | }
41 |
42 | public void Dispose() {
43 | Dispose(true);
44 | global::System.GC.SuppressFinalize(this);
45 | }
46 |
47 | protected virtual void Dispose(bool disposing) {
48 | lock(this) {
49 | if (swigCPtr.Handle != global::System.IntPtr.Zero) {
50 | if (swigCMemOwn) {
51 | swigCMemOwn = false;
52 | EmotivCortexLibPINVOKE.delete_ResponseHandlerCpp(swigCPtr);
53 | }
54 | swigCPtr = new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero);
55 | }
56 | }
57 | }
58 |
59 | public virtual void processResponse(string responseMessage) {
60 | EmotivCortexLibPINVOKE.ResponseHandlerCpp_processResponse(swigCPtr, responseMessage);
61 | if (EmotivCortexLibPINVOKE.SWIGPendingException.Pending) throw EmotivCortexLibPINVOKE.SWIGPendingException.Retrieve();
62 | }
63 |
64 | public ResponseHandlerCpp() : this(EmotivCortexLibPINVOKE.new_ResponseHandlerCpp(), true) {
65 | SwigDirectorConnect();
66 | }
67 |
68 | private void SwigDirectorConnect() {
69 | if (SwigDerivedClassHasMethod("processResponse", swigMethodTypes0))
70 | swigDelegate0 = new SwigDelegateResponseHandlerCpp_0(SwigDirectorMethodprocessResponse);
71 | EmotivCortexLibPINVOKE.ResponseHandlerCpp_director_connect(swigCPtr, swigDelegate0);
72 | }
73 |
74 | private bool SwigDerivedClassHasMethod(string methodName, global::System.Type[] methodTypes) {
75 | global::System.Reflection.MethodInfo[] methodInfos = this.GetType().GetMethods(
76 | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance);
77 | foreach (global::System.Reflection.MethodInfo methodInfo in methodInfos) {
78 | if (methodInfo.DeclaringType == null)
79 | continue;
80 |
81 | if (methodInfo.Name != methodName)
82 | continue;
83 |
84 | var parameters = methodInfo.GetParameters();
85 | if (parameters.Length != methodTypes.Length)
86 | continue;
87 |
88 | bool parametersMatch = true;
89 | for (var i = 0; i < parameters.Length; i++) {
90 | if (parameters[i].ParameterType != methodTypes[i]) {
91 | parametersMatch = false;
92 | break;
93 | }
94 | }
95 |
96 | if (!parametersMatch)
97 | continue;
98 |
99 | if (methodInfo.IsVirtual && (methodInfo.DeclaringType.IsSubclassOf(typeof(ResponseHandlerCpp))) &&
100 | methodInfo.DeclaringType != methodInfo.GetBaseDefinition().DeclaringType) {
101 | return true;
102 | }
103 | }
104 |
105 | return false;
106 | }
107 |
108 | private void SwigDirectorMethodprocessResponse(string responseMessage) {
109 | processResponse(responseMessage);
110 | }
111 |
112 | public delegate void SwigDelegateResponseHandlerCpp_0(string responseMessage);
113 |
114 | private SwigDelegateResponseHandlerCpp_0 swigDelegate0;
115 |
116 | private static global::System.Type[] swigMethodTypes0 = new global::System.Type[] { typeof(string) };
117 | }
118 |
--------------------------------------------------------------------------------
/Src/DataBuffer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections;
4 | using EmotivUnityPlugin;
5 |
6 | ///
7 | /// Data buffer.
8 | ///
9 | public class DataBuffer
10 | {
11 | ///
12 | /// Seting data buffer.
13 | ///
14 | public virtual void SettingBuffer(int winSize, int step, int headerCount) {
15 | UnityEngine.Debug.Log("SettingBuffer");
16 | }
17 |
18 | public virtual double[] GetDataFromBuffer(int index)
19 | {
20 | return null;
21 | }
22 |
23 | ///
24 | /// Get latest data from buffer.
25 | ///
26 | public virtual double[] GetLatestDataFromBuffer(int index)
27 | {
28 | return null;
29 | }
30 |
31 | ///
32 | /// Add data to buffer.
33 | ///
34 | public virtual void AddDataToBuffer(ArrayList data)
35 | {
36 | UnityEngine.Debug.Log("AddDataToBuffer");
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/Src/Editor/ExportUnityPackage.cs:
--------------------------------------------------------------------------------
1 | // filepath: Assets/Editor/ExportUnityPackage.cs
2 | using UnityEditor;
3 |
4 | public class ExportUnityPackage
5 | {
6 | [MenuItem("Tools/Export EmotivUnityPlugin")]
7 | public static void Export()
8 | {
9 | AssetDatabase.ExportPackage(
10 | "Assets/EmotivUnityPlugin",
11 | "EmotivUnityPlugin.unitypackage",
12 | ExportPackageOptions.Recurse
13 | );
14 | UnityEngine.Debug.Log("Exported EmotivUnityPlugin.unitypackage");
15 | }
16 | }
--------------------------------------------------------------------------------
/Src/EegMotionDataBuffer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace EmotivUnityPlugin
7 | {
8 | ///
9 | /// Buffer for eeg data or motion data .
10 | ///
11 | public class EegMotionDataBuffer : DataBuffer
12 | {
13 | BufferStream[] bufHi;
14 |
15 | public enum DataType : int {
16 | EEG, MOTION
17 | }
18 |
19 | List _channels = new List();
20 | DataType _dataType = DataType.EEG;
21 |
22 | public List DataChannels { get => _channels; set => _channels = value; }
23 |
24 | public void Clear() {
25 |
26 | if (bufHi != null) {
27 | Array.Clear(bufHi, 0, bufHi.Length);
28 | bufHi = null;
29 | }
30 | }
31 | public void SetChannels( JArray channelList)
32 | {
33 | _channels.Add(Channel_t.CHAN_TIME_SYSTEM);
34 | foreach(var item in channelList) {
35 | string chanStr = item.ToString();
36 | if (chanStr != "MARKERS") // remove MARKERS chan from eeg header
37 | _channels.Add(ChannelStringList.StringToChannel(chanStr));
38 | }
39 | }
40 | public void SetDataType(DataType type)
41 | {
42 | _dataType = type;
43 | }
44 |
45 | public override void SettingBuffer(int winSize, int step, int headerCount)
46 | {
47 | int buffSize;
48 | if (_dataType == DataType.EEG)
49 | buffSize = headerCount; // include "TIMESTAMP", exclude MARKERS channel
50 | else
51 | buffSize = headerCount + 1; // include "TIMESTAMP" channel
52 |
53 | bufHi = new BufferStream[buffSize];
54 | for (int i = 0; i < buffSize; i++)
55 | {
56 | if (bufHi[i] == null){
57 | bufHi[i] = new BufferStream(winSize, step);
58 | }
59 | else {
60 | bufHi[i].Reset();
61 | bufHi[i].WindowSize = winSize;
62 | bufHi[i].StepSize = step;
63 | }
64 | }
65 | }
66 |
67 | public override void AddDataToBuffer(ArrayList data)
68 | {
69 | if (data.Count > _channels.Count) {
70 | UnityEngine.Debug.Log("AddDataToBuffer: data contain markers channels.");
71 | }
72 |
73 | for (int i=0 ; i < _channels.Count; i++) {
74 | if (data[i] != null) {
75 | double eegData = Convert.ToDouble(data[i]);
76 | bufHi[i].AppendData(eegData);
77 | }
78 | }
79 | }
80 |
81 | // Event handler
82 | public void OnDataReceived(object sender, ArrayList data) {
83 | AddDataToBuffer(data);
84 | }
85 |
86 | public override double[] GetDataFromBuffer(int index)
87 | {
88 | return bufHi[index].NextWithRemoval();
89 | }
90 | public override double[] GetLatestDataFromBuffer(int index)
91 | {
92 | double[] nextSegment = null;
93 | double[] lastSegment = null;
94 | do
95 | {
96 | lastSegment = nextSegment;
97 | nextSegment = GetDataFromBuffer(index);
98 | }
99 | while (nextSegment != null);
100 | return lastSegment;
101 | }
102 |
103 | public double[] GetAllDataFromBuffer(int index)
104 | {
105 | List dataList = new List();
106 | double[] nextSegment = null;
107 | do {
108 | nextSegment = GetDataFromBuffer(index);
109 | if(nextSegment != null)
110 | dataList.AddRange(nextSegment);
111 | }
112 | while (nextSegment != null);
113 | return dataList.ToArray();
114 | }
115 |
116 | // get data both eeg and motion data
117 | public double[] GetData(Channel_t channel)
118 | {
119 | if (channel == Channel_t.CHAN_FLEX_CMS || channel == Channel_t.CHAN_FLEX_DRL)
120 | return null;
121 |
122 | try {
123 | return GetAllDataFromBuffer(GetChanIndex(channel));
124 | }
125 | catch (System.Exception e) {
126 | UnityEngine.Debug.Log(" exception " + e.Message + " index " + GetChanIndex(channel)
127 | + " chan " + (int)channel + " buffSize " + bufHi.Length);
128 | return null;
129 | }
130 | }
131 |
132 | public int GetBufferSize()
133 | {
134 | if(bufHi[3] == null)
135 | return 0;
136 |
137 | return bufHi[3].GetBufSize(); // get buffer size of AF3
138 | }
139 |
140 | public int GetChanIndex(Channel_t chan)
141 | {
142 | int chanIndex = _channels.IndexOf(chan);
143 | return (int)chanIndex;
144 | }
145 |
146 | public void PrintEEgData()
147 | {
148 | double[] eeg = GetData(Channel_t.CHAN_AF3);
149 | UnityEngine.Debug.Log("======PrintEEgData: AF3: size: "
150 | + eeg.Length + " [0]: " + eeg[0].ToString() );
151 | }
152 | }
153 | }
154 |
155 |
--------------------------------------------------------------------------------
/Src/Headset.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json.Linq;
2 | using System.Collections;
3 | using System;
4 | using UnityEngine;
5 |
6 | namespace EmotivUnityPlugin
7 | {
8 | public class Headset
9 | {
10 | private string _headsetID;
11 | private string _status;
12 | private string _serialId;
13 | private string _firmwareVersion;
14 | private string _dongleSerial;
15 | private ArrayList _sensors;
16 | private ArrayList _motionSensors;
17 | private JObject _settings;
18 | private ConnectionType _connectedBy;
19 | private HeadsetTypes _headsetType;
20 | private string _mode;
21 |
22 | // Contructor
23 | public Headset()
24 | {
25 | }
26 | public Headset (JObject jHeadset)
27 | {
28 | HeadsetID = (string)jHeadset["id"];
29 |
30 | if (HeadsetID.Contains(HeadsetNames.epoc_plus))
31 | {
32 | HeadsetType = HeadsetTypes.HEADSET_TYPE_EPOC_PLUS;
33 | }
34 | else if (HeadsetID.Contains(HeadsetNames.epoc_flex))
35 | {
36 | HeadsetType = HeadsetTypes.HEADSET_TYPE_EPOC_FLEX;
37 | }
38 | else if (HeadsetID.Contains(HeadsetNames.epoc_x))
39 | {
40 | HeadsetType = HeadsetTypes.HEADSET_TYPE_EPOC_X;
41 | }
42 | else if (HeadsetID.Contains(HeadsetNames.insight2))
43 | {
44 | HeadsetType = HeadsetTypes.HEADSET_TYPE_INSIGHT2;
45 | }
46 | else if (HeadsetID.Contains(HeadsetNames.insight))
47 | {
48 | HeadsetType = HeadsetTypes.HEADSET_TYPE_INSIGHT;
49 | }
50 | else if (HeadsetID.Contains(HeadsetNames.mn8))
51 | {
52 | HeadsetType = HeadsetTypes.HEADSET_TYPE_MN8;
53 | }
54 | else if (HeadsetID.Contains(HeadsetNames.mw20))
55 | {
56 | HeadsetType = HeadsetTypes.HEADSET_TYPE_MW20;
57 | }
58 | else if (HeadsetID.Contains(HeadsetNames.xtrode))
59 | {
60 | HeadsetType = HeadsetTypes.HEADSET_TYPE_XTRODE;
61 | }
62 | else if (HeadsetID.Contains(HeadsetNames.epoc))
63 | {
64 | HeadsetType = HeadsetTypes.HEADSET_TYPE_EPOC_STD;
65 | }
66 | else if (HeadsetID.Contains(HeadsetNames.flex2))
67 | {
68 | HeadsetType = HeadsetTypes.HEADSET_TYPE_FLEX2;
69 | }
70 |
71 | Status = (string)jHeadset["status"];
72 | FirmwareVersion = (string)jHeadset["firmware"];
73 | DongleSerial = (string)jHeadset["dongle"];
74 | Sensors = new ArrayList();
75 |
76 | foreach (JToken sensor in (JArray)jHeadset["sensors"])
77 | {
78 | Sensors.Add(sensor.ToString());
79 | }
80 | MotionSensors = new ArrayList();
81 | foreach (JToken sensor in (JArray)jHeadset["motionSensors"])
82 | {
83 | MotionSensors.Add(sensor.ToString());
84 | }
85 | Mode = (string)jHeadset["mode"];
86 | string cnnBy = (string)jHeadset["connectedBy"];
87 | if (cnnBy == "dongle") {
88 | HeadsetConnection = ConnectionType.CONN_TYPE_DONGLE;
89 | }
90 | else if (cnnBy == "nRF bluetooth") {
91 | HeadsetConnection = ConnectionType.CONN_TYPE_NRF_BLUETOOTH;
92 | }
93 | else if (cnnBy == "bluetooth") {
94 | HeadsetConnection = ConnectionType.CONN_TYPE_BTLE;
95 | }
96 | else if (cnnBy == "extender") {
97 | HeadsetConnection = ConnectionType.CONN_TYPE_EXTENDER;
98 | }
99 | else if (cnnBy == "usb cable") {
100 | HeadsetConnection = ConnectionType.CONN_TYPE_USB_CABLE;
101 | }
102 | else {
103 | HeadsetConnection = ConnectionType.CONN_TYPE_UNKNOWN;
104 | }
105 | Settings = (JObject)jHeadset["settings"];
106 | }
107 |
108 | // Properties
109 | public string HeadsetID
110 | {
111 | get {
112 | return _headsetID;
113 | }
114 |
115 | set {
116 | _headsetID = value;
117 | }
118 | }
119 |
120 | public HeadsetTypes HeadsetType
121 | {
122 | get {
123 | return _headsetType;
124 | }
125 |
126 | set {
127 | _headsetType = value;
128 | }
129 | }
130 |
131 | public string Status
132 | {
133 | get {
134 | return _status;
135 | }
136 |
137 | set {
138 | _status = value;
139 | }
140 | }
141 |
142 | public string SerialId
143 | {
144 | get {
145 | return _serialId;
146 | }
147 |
148 | set {
149 | _serialId = value;
150 | }
151 | }
152 |
153 | public string FirmwareVersion
154 | {
155 | get {
156 | return _firmwareVersion;
157 | }
158 |
159 | set {
160 | _firmwareVersion = value;
161 | }
162 | }
163 |
164 | public string DongleSerial
165 | {
166 | get {
167 | return _dongleSerial;
168 | }
169 |
170 | set {
171 | _dongleSerial = value;
172 | }
173 | }
174 |
175 | public ArrayList Sensors
176 | {
177 | get {
178 | return _sensors;
179 | }
180 |
181 | set {
182 | _sensors = value;
183 | }
184 | }
185 |
186 | public ArrayList MotionSensors
187 | {
188 | get {
189 | return _motionSensors;
190 | }
191 |
192 | set {
193 | _motionSensors = value;
194 | }
195 | }
196 |
197 | public JObject Settings
198 | {
199 | get {
200 | return _settings;
201 | }
202 |
203 | set {
204 | _settings = value;
205 | }
206 | }
207 |
208 | public ConnectionType HeadsetConnection
209 | {
210 | get {
211 | return _connectedBy;
212 | }
213 |
214 | set {
215 | _connectedBy = value;
216 | }
217 | }
218 |
219 | public string Mode
220 | {
221 | get {
222 | return _mode;
223 | }
224 |
225 | set {
226 | _mode = value;
227 | }
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/Src/HeadsetFinder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Newtonsoft.Json.Linq;
5 | using System.Timers;
6 | using UnityEngine;
7 |
8 | namespace EmotivUnityPlugin
9 | {
10 | ///
11 | /// Reponsible for finding headsets.
12 | ///
13 | public class HeadsetFinder
14 | {
15 | private CortexClient _ctxClient = CortexClient.Instance;
16 |
17 | ///
18 | /// Timer for querying headsets
19 | ///
20 | private Timer _aTimer = null;
21 |
22 | // Event
23 | public event EventHandler HeadsetDisConnectedOK;
24 | public event EventHandler> QueryHeadsetOK;
25 |
26 | public HeadsetFinder()
27 | {
28 | _ctxClient = CortexClient.Instance;
29 | _ctxClient.QueryHeadsetOK += OnQueryHeadsetReceived;
30 | _ctxClient.HeadsetDisConnectedOK += OnHeadsetDisconnectedOK;
31 | }
32 |
33 | public static HeadsetFinder Instance { get; } = new HeadsetFinder();
34 |
35 | private void OnHeadsetDisconnectedOK(object sender, bool e)
36 | {
37 | HeadsetDisConnectedOK(this, true);
38 | }
39 |
40 | private void OnQueryHeadsetReceived(object sender, List headsets)
41 | {
42 | QueryHeadsetOK(this, headsets);
43 | }
44 |
45 | ///
46 | /// Init headset finder
47 | ///
48 | public void FinderInit()
49 | {
50 | SetQueryHeadsetTimer();
51 | }
52 |
53 | public void StopQueryHeadset() {
54 | if (_aTimer != null && _aTimer.Enabled) {
55 | UnityEngine.Debug.Log("Stop query headset");
56 | _aTimer.Stop();
57 | }
58 | }
59 | public void RefreshHeadset() {
60 | _ctxClient.ControlDevice("refresh", "", null);
61 | }
62 |
63 | ///
64 | /// Setup query headset timer
65 | ///
66 | private void SetQueryHeadsetTimer()
67 | {
68 | if (_aTimer != null) {
69 | _aTimer.Enabled = true;
70 | return;
71 | }
72 |
73 | _aTimer = new Timer(Config.QUERY_HEADSET_TIME);
74 |
75 | // Hook up the Elapsed event for the timer.
76 | _aTimer.Elapsed += OnTimedEvent;
77 | _aTimer.AutoReset = true;
78 | _aTimer.Enabled = true;
79 | }
80 |
81 | ///
82 | /// Handle timeout. Retry query headsets.
83 | ///
84 | private void OnTimedEvent(object sender, ElapsedEventArgs e)
85 | {
86 | _ctxClient.QueryHeadsets("");
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Src/IdentityModel/IdentityModel.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/IdentityModel/IdentityModel.dll
--------------------------------------------------------------------------------
/Src/IosPlugin/CortexLibIosEmbeddedConnection.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface CortexLibIosEmbeddedConnection : NSObject {
5 | @private
6 | CortexClient *cortexClient;
7 | }
8 |
9 | + (id _Nonnull) shareInstance;
10 | - (void)sendRequest:(NSString * _Nonnull)nsJsonString;
11 | - (void)close;
12 |
13 | # pragma mark CortexClientDelegate
14 | - (void) processResponse:(NSString * _Nonnull)responseMessage;
15 | @end
16 |
--------------------------------------------------------------------------------
/Src/IosPlugin/CortexLibIosEmbeddedConnection.m:
--------------------------------------------------------------------------------
1 | #import "CortexLibIosEmbeddedConnection.h"
2 |
3 | static CortexLibIosEmbeddedConnection *sharedInstance = nil;
4 |
5 | typedef void (*UnityResponseCallback)(const char* _Nonnull responseMessage);
6 | static UnityResponseCallback unityResponseCb = NULL;
7 |
8 | //register callback from unity
9 | void RegisterUnityResponseCallback(UnityResponseCallback callback) {
10 | unityResponseCb = callback;
11 | }
12 |
13 | @implementation CortexLibIosEmbeddedConnection
14 |
15 | +(id) shareInstance {
16 | if(!sharedInstance) {
17 | sharedInstance = [[CortexLibIosEmbeddedConnection alloc] initAfterCortexStarted];
18 | }
19 | return sharedInstance;
20 | }
21 |
22 |
23 | -(id) initAfterCortexStarted {
24 | self = [super init];
25 | if(self) {
26 | cortexClient = [[CortexClient alloc] init];
27 | cortexClient.delegate = self;
28 | }
29 | return self;
30 | }
31 |
32 | -(void)sendRequest:(NSString *)nsJsonString {
33 | [cortexClient sendRequest:nsJsonString];
34 | }
35 |
36 | -(void)close {
37 | [cortexClient close];
38 | }
39 |
40 | -(void)processResponse:(NSString *)responseMessage {
41 | if (unityResponseCb != NULL) {
42 | const char* cString = [responseMessage UTF8String];
43 | unityResponseCb(cString);
44 | }
45 | }
46 | @end
--------------------------------------------------------------------------------
/Src/IosPlugin/CortexLibIosWrapper.m:
--------------------------------------------------------------------------------
1 | #import "CortexLibIosEmbeddedConnection.h"
2 |
3 | #import
4 | #include
5 |
6 | typedef void (*UnityStartedCallback)(void);
7 | static UnityStartedCallback startedCb = NULL;
8 |
9 | void RegisterUnityStartedCallback(UnityStartedCallback callback) {
10 | startedCb = callback;
11 | }
12 |
13 | bool InitCortexLib() {
14 | NSLog(@"operatingSystemVersionString %@", [[NSProcessInfo processInfo] operatingSystemVersionString]);
15 |
16 | [CortexLib start:^(void){
17 | NSLog(@"CortexLib iOS started");
18 | [CortexLibIosEmbeddedConnection shareInstance];
19 | if (startedCb != NULL) {
20 | startedCb();
21 | }
22 | }];
23 | return true;
24 | }
25 |
26 | void StopCortexLib() {
27 | [[CortexLibIosEmbeddedConnection shareInstance] close];
28 | [CortexLib stop];
29 | }
30 |
31 | void SendRequest(const char* requestJson) {
32 | NSString *request = [NSString stringWithUTF8String:requestJson];
33 | [[CortexLibIosEmbeddedConnection shareInstance] sendRequest:request];
34 | }
--------------------------------------------------------------------------------
/Src/IosPlugin/README.md:
--------------------------------------------------------------------------------
1 | # Emotiv Unity Plugin - iOS Integration Guide
2 |
3 | ## Download and Setup
4 | Download the most recent version of [EmotivCortexLib](https://github.com/Emotiv/cortex-embedded-lib-example/releases) and place in this directory
5 | ## Integration
6 | In your Unity project, integrate the following functions to interface with CortexLib via the EmotivUnityPlugin
7 | - bool InitCortexLib();
8 | - void StopCortexLib();
9 | - void RegisterUnityResponseCallback(MessageCallback callback);
10 | - void SendRequest(string request);
--------------------------------------------------------------------------------
/Src/JsonNet/Newtonsoft.Json.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/JsonNet/Newtonsoft.Json.dll
--------------------------------------------------------------------------------
/Src/MentalStateModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json.Linq;
3 |
4 | namespace EmotivUnityPlugin
5 | {
6 | public struct MentalStateModel
7 | {
8 | public float totalDuration;
9 | public float overload; // duration of overload (burnout) state
10 | public float disengaged;
11 | public float flow;
12 | public float intense;
13 | public float moderate;
14 | public float optimal;
15 |
16 | public MentalStateModel(JObject jsonObject)
17 | {
18 | if (jsonObject == null || jsonObject.Count == 0)
19 | {
20 | totalDuration = 0;
21 | overload = 0;
22 | disengaged = 0;
23 | flow = 0;
24 | intense = 0;
25 | moderate = 0;
26 | optimal = 0;
27 | return;
28 | }
29 |
30 | totalDuration = (float)jsonObject["totalDuration"];
31 | overload = 0;
32 | disengaged = 0;
33 | flow = 0;
34 | intense = 0;
35 | moderate = 0;
36 | optimal = 0;
37 |
38 | JArray states = (JArray)jsonObject["states"];
39 | foreach (JObject state in states)
40 | {
41 | string stateName = (string)state["state"];
42 | float duration = (float)state["duration"];
43 |
44 | switch (stateName)
45 | {
46 | case "burnout":
47 | overload = duration;
48 | break;
49 | case "disengaged":
50 | disengaged = duration;
51 | break;
52 | case "flow":
53 | flow = duration;
54 | break;
55 | case "intense":
56 | intense = duration;
57 | break;
58 | case "moderate":
59 | moderate = duration;
60 | break;
61 | case "optimal":
62 | optimal = duration;
63 | break;
64 | }
65 | }
66 | }
67 |
68 | // create to string
69 | public string ToString()
70 | {
71 | return "TotalDuration: " + totalDuration + "\n" +
72 | "Overload: " + overload + "\n" +
73 | "Disengaged: " + disengaged + "\n" +
74 | "Flow: " + flow + "\n" +
75 | "Intense: " + intense + "\n" +
76 | "Moderate: " + moderate + "\n" +
77 | "Optimal: " + optimal + "\n";
78 | }
79 |
80 | public float[] GetPercentages()
81 | {
82 | float[] percentages = new float[6];
83 | if (totalDuration > 0)
84 | {
85 | percentages[0] = (disengaged / totalDuration);
86 | percentages[1] = (moderate / totalDuration);
87 | percentages[2] = (flow / totalDuration);
88 | percentages[3] = (optimal / totalDuration);
89 | percentages[4] = (intense / totalDuration);
90 | percentages[5] = (overload / totalDuration);
91 |
92 | // Round to two decimal places
93 | for (int i = 0; i < percentages.Length; i++)
94 | {
95 | percentages[i] = (float)Math.Round(percentages[i], 2);
96 | }
97 |
98 | // Adjust to ensure the total is 1.0
99 | float total = 0;
100 | for (int i = 0; i < percentages.Length; i++)
101 | {
102 | total += percentages[i];
103 | }
104 |
105 | if (total != 1.0f)
106 | {
107 | float difference = 1.0f - total;
108 | percentages[0] += difference; // Adjust the first element to make the total 1.0
109 | }
110 | }
111 | return percentages;
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Src/MyLogger.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 | using System.Collections;
3 | using System.IO;
4 | using System;
5 |
6 | namespace EmotivUnityPlugin
7 | {
8 | ///
9 | /// Logger handler: print log at file with format
10 | /// Not apply for unity editor mode.
11 | ///
12 | public class MyLogger : ILogger
13 | {
14 | static readonly object _object = new object();
15 | private FileStream m_FileStream;
16 | private StreamWriter m_StreamWriter;
17 | private ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler;
18 | public static MyLogger Instance { get; } = new MyLogger();
19 |
20 | public ILogHandler logHandler { get; set; }
21 | public bool logEnabled { get; set; }
22 | public LogType filterLogType { get; set; }
23 | public bool saveToFile { get; set; }
24 | public bool showConsoleLog { get; set; }
25 |
26 | private MyLogger()
27 | {
28 | logHandler = this;
29 | logEnabled = true;
30 | filterLogType = LogType.Log;
31 | saveToFile = true; // Default to saving logs to files
32 | showConsoleLog = true; // Default to showing logs in the console
33 | }
34 |
35 | ///
36 | /// Initial logger handler
37 | ///
38 | public void Init(string prefixFileName , bool saveToFile)
39 | {
40 | this.saveToFile = saveToFile;
41 |
42 | if (saveToFile)
43 | {
44 | string dateTimeStr = DateTime.Now.ToString("yyyyMMdd_HHmmss");
45 | string fileName = prefixFileName + "Log_" + dateTimeStr + ".txt";
46 |
47 | string logPath = Config.LogDirectory;
48 | string filePath = Path.Combine(logPath, fileName);
49 | m_FileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
50 | m_StreamWriter = new StreamWriter(m_FileStream);
51 | }
52 |
53 | // Replace the default debug log handler
54 | UnityEngine.Debug.unityLogger.logHandler = this;
55 | }
56 |
57 | public void Log(LogType logType, object message)
58 | {
59 | if (logEnabled && logType <= filterLogType)
60 | {
61 | logHandler.LogFormat(logType, null, "{0}", message);
62 | }
63 | }
64 |
65 | public void Log(LogType logType, object message, UnityEngine.Object context)
66 | {
67 | if (logEnabled && logType <= filterLogType)
68 | {
69 | logHandler.LogFormat(logType, context, "{0}", message);
70 | }
71 | }
72 |
73 | public void Log(LogType logType, string tag, object message)
74 | {
75 | if (logEnabled && logType <= filterLogType)
76 | {
77 | logHandler.LogFormat(logType, null, "{0}: {1}", tag, message);
78 | }
79 | }
80 |
81 | public void Log(LogType logType, string tag, object message, UnityEngine.Object context)
82 | {
83 | if (logEnabled && logType <= filterLogType)
84 | {
85 | logHandler.LogFormat(logType, context, "{0}: {1}", tag, message);
86 | }
87 | }
88 |
89 | public void Log(object message)
90 | {
91 | Log(LogType.Log, message);
92 | }
93 |
94 | public void Log(string tag, object message)
95 | {
96 | Log(LogType.Log, tag, message);
97 | }
98 |
99 | public void Log(string tag, object message, UnityEngine.Object context)
100 | {
101 | Log(LogType.Log, tag, message, context);
102 | }
103 |
104 | public void LogWarning(string tag, object message)
105 | {
106 | Log(LogType.Warning, tag, message);
107 | }
108 |
109 | public void LogWarning(string tag, object message, UnityEngine.Object context)
110 | {
111 | Log(LogType.Warning, tag, message, context);
112 | }
113 |
114 | public void LogError(string tag, object message)
115 | {
116 | Log(LogType.Error, tag, message);
117 | }
118 |
119 | public void LogError(string tag, object message, UnityEngine.Object context)
120 | {
121 | Log(LogType.Error, tag, message, context);
122 | }
123 |
124 | public void LogException(Exception exception)
125 | {
126 | LogException(exception, null);
127 | }
128 |
129 | public void LogException(Exception exception, UnityEngine.Object context)
130 | {
131 | if (logEnabled && LogType.Exception <= filterLogType)
132 | {
133 | logHandler.LogException(exception, context);
134 | }
135 | }
136 |
137 | public void LogFormat(LogType logType, UnityEngine.Object context, string format, params object[] args)
138 | {
139 | lock (_object)
140 | {
141 | LogType myLogType = logType;
142 | string type = "";
143 | switch (logType)
144 | {
145 | case LogType.Log:
146 | type = "info";
147 | break;
148 | case LogType.Warning:
149 | type = "warning";
150 | myLogType = LogType.Log;
151 | break;
152 | case LogType.Error:
153 | type = "error";
154 | myLogType = LogType.Log;
155 | break;
156 | case LogType.Exception:
157 | type = "exception";
158 | break;
159 | default:
160 | type = "info";
161 | break;
162 | }
163 |
164 | string newFormat;
165 | object[] tmpArgs;
166 |
167 | if (args.Length > 1 && args[0].ToString() == "CortexLog")
168 | {
169 | newFormat = "{0}"; // log from cortex side
170 | tmpArgs = new object[1];
171 | tmpArgs[0] = args[1]; // get second element of args for message
172 | }
173 | else
174 | {
175 | newFormat = "[{0}][unity " + type + " ] {1}"; // log from unity side
176 | string dtNow = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffzzz");
177 | tmpArgs = new object[2];
178 | tmpArgs[0] = dtNow;
179 | tmpArgs[1] = args[0]; // only get first element of args
180 | }
181 |
182 | if (saveToFile)
183 | {
184 | m_StreamWriter.WriteLine(String.Format(newFormat, tmpArgs));
185 | m_StreamWriter.Flush();
186 | }
187 |
188 | if (showConsoleLog)
189 | {
190 | m_DefaultLogHandler.LogFormat(myLogType, context, newFormat, tmpArgs);
191 | }
192 | }
193 | }
194 |
195 | public bool IsLogTypeAllowed(LogType logType)
196 | {
197 | return logEnabled && logType <= filterLogType;
198 | }
199 |
200 | public void LogFormat(LogType logType, string format, params object[] args)
201 | {
202 | if (IsLogTypeAllowed(logType))
203 | {
204 | lock (_object)
205 | {
206 | LogFormat(logType, null, format, args);
207 | }
208 | }
209 | }
210 | }
211 | }
--------------------------------------------------------------------------------
/Src/PMDataBuffer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace EmotivUnityPlugin
7 | {
8 |
9 | ///
10 | /// Performance metric data buffer .
11 | ///
12 | public class PMDataBuffer : DataBuffer
13 | {
14 | BufferStream[] bufHi; // high rate buffer
15 |
16 | const int InvalidValue = -1; // for null data value when is poor EEG signal quality
17 |
18 | List _pmList = new List(); // performance metric lists
19 |
20 | public List PmList { get => _pmList; set => _pmList = value; }
21 |
22 | public int SetChannels(JArray pmLists)
23 | {
24 | string timestamp = ChannelStringList.ChannelToString(Channel_t.CHAN_TIME_SYSTEM);
25 | int count = 1;
26 | PmList.Add(timestamp);
27 | foreach(var item in pmLists){
28 | // exclude Active flag
29 | string chanStr = item.ToString();
30 | if (!chanStr.Contains(".isActive")) {
31 | PmList.Add(chanStr);
32 | ++count;
33 | }
34 | }
35 | return count;
36 | }
37 |
38 | public void Clear() {
39 |
40 | if (bufHi != null) {
41 | Array.Clear(bufHi, 0, bufHi.Length);
42 | bufHi = null;
43 | }
44 | }
45 |
46 | public override void SettingBuffer(int winSize, int step, int headerCount) {
47 | int buffSize = headerCount;
48 | bufHi = new BufferStream[buffSize];
49 | // UnityEngine.Debug.Log("PM Setting Buffer size" + bufHi.Length);
50 | for (int i = 0; i < buffSize; i++)
51 | {
52 | if (bufHi[i] == null){
53 | bufHi[i] = new BufferStream(winSize, step);
54 | }
55 | else {
56 | bufHi[i].Reset();
57 | bufHi[i].WindowSize = winSize;
58 | bufHi[i].StepSize = step;
59 | }
60 | }
61 | }
62 |
63 | // event handler
64 | public void OnPMDataReceived(object sender, ArrayList data)
65 | {
66 | AddDataToBuffer(data);
67 | }
68 |
69 | public override void AddDataToBuffer(ArrayList data)
70 | {
71 | int i = 0;
72 | foreach (var ele in data) {
73 | // ignore active flag
74 | if (Utils.IsNumericType(ele))
75 | {
76 | try
77 | {
78 | double pmData = Convert.ToDouble(ele);
79 | bufHi[i].AppendData(pmData);
80 | i++;
81 | }
82 | catch (System.Exception e)
83 | {
84 | UnityEngine.Debug.LogError(e.Message + " index " + i + " value " +ele.ToString());
85 | break;
86 | }
87 |
88 | }
89 | }
90 | }
91 | public override double[] GetDataFromBuffer(int index)
92 | {
93 | return bufHi[index].NextWithRemoval();
94 | }
95 | public override double[] GetLatestDataFromBuffer(int index)
96 | {
97 | double[] nextSegment = null;
98 | double[] lastSegment = null;
99 | do
100 | {
101 | lastSegment = nextSegment;
102 | nextSegment = GetDataFromBuffer(index);
103 | }
104 | while (nextSegment != null);
105 | return lastSegment;
106 | }
107 |
108 | public double GetData(string label)
109 | {
110 | int index = GetLabelIndex(label);
111 | if (index == -1) {
112 | UnityEngine.Debug.LogError(" Invalid label: " + label);
113 | return InvalidValue;
114 | }
115 | double[] chanData = GetLatestDataFromBuffer(index);
116 | if (chanData != null) {
117 | return chanData[0];
118 | } else {
119 | return InvalidValue;
120 | }
121 | }
122 |
123 | public int GetLabelIndex(string chan)
124 | {
125 | int chanIndex = _pmList.IndexOf(chan);
126 | return (int)chanIndex;
127 | }
128 |
129 | public int GetBufferSize()
130 | {
131 | if(bufHi[1] == null)
132 | return 0;
133 |
134 | return bufHi[1].GetBufSize(); // buff size of "boredom"
135 | }
136 | }
137 | }
138 |
139 |
140 |
--------------------------------------------------------------------------------
/Src/PostProcessBuild/PostProcessBuild.Editor.asmdef:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PostProcessBuild.Editor",
3 | "references": [],
4 | "includePlatforms": ["Editor"],
5 | "excludePlatforms": [],
6 | "allowUnsafeCode": false,
7 | "overrideReferences": false,
8 | "precompiledReferences": [],
9 | "autoReferenced": true,
10 | "defineConstraints": ["UNITY_IOS"],
11 | "versionDefines": [],
12 | "noEngineReferences": false
13 | }
--------------------------------------------------------------------------------
/Src/PostProcessBuild/PostProcessBuild.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_IOS
2 | using UnityEditor;
3 | using UnityEditor.Callbacks;
4 | using UnityEditor.iOS.Xcode;
5 | using UnityEditor.iOS.Xcode.Extensions;
6 | using System.IO;
7 |
8 | public class PostProcessBuild
9 | {
10 | [PostProcessBuild]
11 | public static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject)
12 | {
13 | if (target == BuildTarget.iOS)
14 | {
15 | string pbxProjectPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
16 | PBXProject pbxProject = new PBXProject();
17 | pbxProject.ReadFromFile(pbxProjectPath);
18 |
19 | string targetGuid = pbxProject.GetUnityMainTargetGuid();
20 | string unityFrameworkTargetGuid = pbxProject.GetUnityFrameworkTargetGuid();
21 |
22 | // Automatic code signing
23 | pbxProject.SetBuildProperty(targetGuid, "CODE_SIGN_STYLE", "Automatic");
24 |
25 | // DEVELOPMENT_TEAM is set from Jenkinsfile (actual team ID)
26 | string developmentTeam = System.Environment.GetEnvironmentVariable("DEVELOPMENT_TEAM");
27 | if (!string.IsNullOrEmpty(developmentTeam))
28 | {
29 | pbxProject.SetBuildProperty(targetGuid, "DEVELOPMENT_TEAM", developmentTeam);
30 | }
31 |
32 | // Add and link EmotivCortexLib.xcframework to main target
33 | string frameworkPath = Path.Combine(pathToBuiltProject, "Frameworks/EmotivCortexLib.xcframework");
34 | string fileGuid = pbxProject.AddFile(frameworkPath, "Frameworks/EmotivCortexLib.xcframework", PBXSourceTree.Source);
35 | pbxProject.AddFileToBuild(targetGuid, fileGuid);
36 | pbxProject.AddFileToBuild(unityFrameworkTargetGuid, fileGuid);
37 |
38 | // Add the framework to the "Embed Frameworks" build phase
39 | string embedPhase = pbxProject.AddCopyFilesBuildPhase(targetGuid, "Embed Frameworks", "", "10");
40 | pbxProject.AddFileToBuildSection(targetGuid, embedPhase, fileGuid);
41 |
42 | // Ensure "Code Sign On Copy" is enabled for the framework
43 | PBXProjectExtensions.AddFileToEmbedFrameworks(pbxProject, targetGuid, fileGuid);
44 |
45 | pbxProject.AddBuildProperty(targetGuid, "FRAMEWORK_SEARCH_PATHS", "$(PROJECT_DIR)/Frameworks");
46 | pbxProject.AddBuildProperty(targetGuid, "OTHER_LDFLAGS", "-framework EmotivCortexLib");
47 | pbxProject.AddBuildProperty(unityFrameworkTargetGuid, "FRAMEWORK_SEARCH_PATHS", "$(PROJECT_DIR)/Frameworks");
48 | pbxProject.AddBuildProperty(unityFrameworkTargetGuid, "OTHER_LDFLAGS", "-framework EmotivCortexLib");
49 |
50 | pbxProject.WriteToFile(pbxProjectPath);
51 |
52 | // Add NSBluetoothAlwaysUsageDescription
53 | string plistPath = Path.Combine(pathToBuiltProject, "Info.plist");
54 | PlistDocument plist = new PlistDocument();
55 | plist.ReadFromFile(plistPath);
56 |
57 | PlistElementDict rootDict = plist.root;
58 | rootDict.SetString("NSBluetoothAlwaysUsageDescription", "This will allow app to find and connect to Bluetooth accessories.");
59 |
60 | plist.WriteToFile(plistPath);
61 | }
62 | }
63 | }
64 | #endif
--------------------------------------------------------------------------------
/Src/RecordManager.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json.Linq;
2 | using System;
3 | using System.Collections.Generic;
4 | using UnityEngine;
5 | using System.Threading;
6 |
7 | namespace EmotivUnityPlugin
8 | {
9 | ///
10 | /// Reponsible for managing and handling records and markers.
11 | ///
12 | public class RecordManager
13 | {
14 | static readonly object _locker = new object();
15 | private CortexClient _ctxClient = CortexClient.Instance;
16 | private Authorizer _authorizer = Authorizer.Instance;
17 | private SessionHandler _sessionHandler = SessionHandler.Instance;
18 |
19 | private string _currMarkerId;
20 |
21 | public static RecordManager Instance { get; } = new RecordManager();
22 |
23 | // Event
24 | public event EventHandler informStartRecordResult;
25 | public event EventHandler informStopRecordResult;
26 |
27 | public event EventHandler informMarkerResult;
28 |
29 | public event EventHandler DataPostProcessingFinished
30 | {
31 | add { _ctxClient.DataPostProcessingFinished += value; }
32 | remove { _ctxClient.DataPostProcessingFinished -= value; }
33 | }
34 |
35 | public event EventHandler ExportRecordsFinished
36 | {
37 | add { _ctxClient.ExportRecordsFinished += value; }
38 | remove { _ctxClient.ExportRecordsFinished -= value; }
39 | }
40 |
41 | // Constructor
42 | public RecordManager ()
43 | {
44 | _sessionHandler.CreateRecordOK += OnCreateRecordOK;
45 | _sessionHandler.StopRecordOK += OnStopRecordOK;
46 | _ctxClient.InjectMarkerOK += OnInjectMarkerOK;
47 | _ctxClient.UpdateMarkerOK += OnUpdateMarkerOK;
48 | }
49 |
50 | private void OnStopRecordOK(object sender, Record record)
51 | {
52 | UnityEngine.Debug.Log("RecordManager: OnStopRecordOK recordId: " + record.Uuid +
53 | " at: " + record.EndDateTime);
54 | informStopRecordResult(this, record);
55 | }
56 |
57 | private void OnCreateRecordOK(object sender, Record record)
58 | {
59 | informStopRecordResult(this, record);
60 | informStartRecordResult(this, record);
61 | }
62 | private void OnInjectMarkerOK(object sender, JObject markerObj)
63 | {
64 | _currMarkerId = markerObj["uuid"].ToString();
65 | informMarkerResult(this, markerObj);
66 | }
67 | private void OnUpdateMarkerOK(object sender, JObject markerObj)
68 | {
69 | informMarkerResult(this, markerObj);
70 | }
71 |
72 | ///
73 | /// Create a new record.
74 | ///
75 | public void StartRecord(string title, string description = null,
76 | string subjectName = null, List tags= null)
77 | {
78 | lock(_locker)
79 | {
80 | // start record
81 | _sessionHandler.StartRecord(_authorizer.CortexToken, title, description, subjectName, tags);
82 | }
83 | }
84 |
85 | ///
86 | /// Stop a record that was previously started by StartRecord
87 | ///
88 | public void StopRecord()
89 | {
90 | lock(_locker)
91 | {
92 | _sessionHandler.StopRecord(_authorizer.CortexToken);
93 | }
94 | }
95 | // TODO: Update Record
96 |
97 | ///
98 | /// inject marker
99 | ///
100 | public void InjectMarker(string markerLabel, string markerValue)
101 | {
102 | lock(_locker)
103 | {
104 | string cortexToken = _authorizer.CortexToken;
105 | string sessionId = _sessionHandler.SessionId;
106 |
107 | // inject marker
108 | _ctxClient.InjectMarker(cortexToken, sessionId, markerLabel, markerValue, Utils.GetEpochTimeNow());
109 | }
110 | }
111 |
112 | ///
113 | /// update marker to set the end date time of a marker, turning an "instance" marker into an "interval" marker
114 | ///
115 | public void UpdateMarker()
116 | {
117 | lock(_locker)
118 | {
119 | string cortexToken = _authorizer.CortexToken;
120 | string sessionId = _sessionHandler.SessionId;
121 |
122 | // update marker
123 | _ctxClient.UpdateMarker(cortexToken, sessionId, _currMarkerId, Utils.GetEpochTimeNow());
124 | }
125 | }
126 |
127 | public void ExportRecord(List records, string folderPath,
128 | List streamTypes, string format, string version = null,
129 | List licenseIds = null, bool includeDemographics = false,
130 | bool includeMarkerExtraInfos = false, bool includeSurvey = false,
131 | bool includeDeprecatedPM = false)
132 | {
133 | _ctxClient.ExportRecord(_authorizer.CortexToken, records, folderPath,
134 | streamTypes, format, version, licenseIds,
135 | includeDemographics, includeMarkerExtraInfos,
136 | includeSurvey, includeDeprecatedPM);
137 | }
138 |
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Src/RegistryConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.IO;
4 | #if UNITY_STANDALONE_WIN && NET_4_6
5 | using Microsoft.Win32; // For registry operations but only supported on Windows with .NET Framework version >= 4.6, not for .NET Standard
6 | #endif
7 | using UnityEngine;
8 |
9 | namespace EmotivUnityPlugin
10 | {
11 | public class RegistryConfig
12 | {
13 | public RegistryConfig(string uriScheme)
14 | {
15 | CustomUriScheme = uriScheme;
16 | }
17 |
18 | public void Configure()
19 | {
20 | #if UNITY_STANDALONE_WIN && NET_4_6
21 | if (NeedToAddKeys()) AddRegKeys();
22 | #else
23 | Debug.LogWarning("RegistryConfig is only supported on Windows and with .NET Framework version >= 4.6, not for .NET Standard");
24 | #endif
25 | }
26 |
27 | private string CustomUriScheme { get; }
28 | #if UNITY_STANDALONE_WIN && NET_4_6
29 | string CustomUriSchemeKeyPath => RootKeyPath + @"\" + CustomUriScheme;
30 | string CustomUriSchemeKeyValueValue => "URL:" + CustomUriScheme;
31 | string CommandKeyPath => CustomUriSchemeKeyPath + @"\shell\open\command";
32 |
33 | const string RootKeyPath = @"Software\Classes";
34 |
35 | const string CustomUriSchemeKeyValueName = "";
36 |
37 | const string ShellKeyName = "shell";
38 | const string OpenKeyName = "open";
39 | const string CommandKeyName = "command";
40 |
41 | const string CommandKeyValueName = "";
42 | const string CommandKeyValueFormat = "\"{0}\\UnityExample.exe\" \"%1\"";
43 | static string CommandKeyValueValue => String.Format(CommandKeyValueFormat, Path.GetDirectoryName(Application.dataPath));
44 |
45 | const string UrlProtocolValueName = "URL Protocol";
46 | const string UrlProtocolValueValue = "";
47 |
48 | bool NeedToAddKeys()
49 | {
50 | var addKeys = false;
51 | using (var commandKey = Registry.CurrentUser.OpenSubKey(CommandKeyPath))
52 | {
53 | var commandValue = commandKey?.GetValue(CommandKeyValueName);
54 | addKeys |= !CommandKeyValueValue.Equals(commandValue);
55 | }
56 |
57 | using (var customUriSchemeKey = Registry.CurrentUser.OpenSubKey(CustomUriSchemeKeyPath))
58 | {
59 | var uriValue = customUriSchemeKey?.GetValue(CustomUriSchemeKeyValueName);
60 | var protocolValue = customUriSchemeKey?.GetValue(UrlProtocolValueName);
61 |
62 | addKeys |= !CustomUriSchemeKeyValueValue.Equals(uriValue);
63 | addKeys |= !UrlProtocolValueValue.Equals(protocolValue);
64 | }
65 | return addKeys;
66 | }
67 |
68 | void AddRegKeys()
69 | {
70 | using (var classesKey = Registry.CurrentUser.OpenSubKey(RootKeyPath, true))
71 | {
72 | using (var root = classesKey!.OpenSubKey(CustomUriScheme, true) ??
73 | classesKey.CreateSubKey(CustomUriScheme, true))
74 | {
75 | root.SetValue(CustomUriSchemeKeyValueName, CustomUriSchemeKeyValueValue);
76 | root.SetValue(UrlProtocolValueName, UrlProtocolValueValue);
77 |
78 | using (var shell = root.OpenSubKey(ShellKeyName, true) ??
79 | root.CreateSubKey(ShellKeyName, true))
80 | {
81 | using (var open = shell.OpenSubKey(OpenKeyName, true) ??
82 | shell.CreateSubKey(OpenKeyName, true))
83 | {
84 | using (var command = open.OpenSubKey(CommandKeyName, true) ??
85 | open.CreateSubKey(CommandKeyName, true))
86 | {
87 | command.SetValue(CommandKeyValueName, CommandKeyValueValue);
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 | #endif
95 | }
96 | }
--------------------------------------------------------------------------------
/Src/SessionHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace EmotivUnityPlugin
5 | {
6 | ///
7 | /// Reponsible for handling sessions and records.
8 | ///
9 | public class SessionHandler
10 | {
11 | static readonly object _locker = new object();
12 | private static string _sessionId = "";
13 | private CortexClient _ctxClient = CortexClient.Instance;
14 |
15 | //event
16 | public event EventHandler SessionActived;
17 | public event EventHandler SessionClosedOK;
18 | public event EventHandler CreateRecordOK;
19 | public event EventHandler StopRecordOK;
20 | public event EventHandler SessionClosedNotify;
21 |
22 | public static SessionHandler Instance { get; } = new SessionHandler();
23 |
24 | ///
25 | /// Gets current SessionId.
26 | ///
27 | /// The current SessionId.
28 | public string SessionId
29 | {
30 | get {
31 | lock (_locker)
32 | {
33 | return _sessionId;
34 | }
35 | }
36 | }
37 |
38 | //Constructor
39 | public SessionHandler()
40 | {
41 | _ctxClient.CreateSessionOK += CreateSessionOk;
42 | _ctxClient.UpdateSessionOK += UpdateSessionOk;
43 | _ctxClient.CreateRecordOK += OnCreateRecordOK;
44 | _ctxClient.UpdateRecordOK += OnUpdateRecordOK;
45 | _ctxClient.StopRecordOK += OnStopRecordOK;
46 | _ctxClient.SessionClosedNotify += OnSessionClosedNotify;
47 | }
48 |
49 | private void OnSessionClosedNotify(object sender, string sessionId)
50 | {
51 | UnityEngine.Debug.Log("SessionHandler: OnSessionClosedNotify " + sessionId);
52 | lock (_locker)
53 | {
54 | if (_sessionId == sessionId) {
55 | // clear session data
56 | _sessionId = "";
57 | SessionClosedNotify(this, sessionId);
58 | }
59 | }
60 |
61 | }
62 |
63 | private void OnStopRecordOK(object sender, Record record)
64 | {
65 | UnityEngine.Debug.Log("OnStopRecordOK: recordId " + record.Uuid);
66 | StopRecordOK(this, record);
67 | }
68 |
69 | private void OnUpdateRecordOK(object sender, Record record)
70 | {
71 | UnityEngine.Debug.Log("OnUpdateRecordOK: recordId " + record.Uuid);
72 | // TODO: emit signal
73 | }
74 |
75 | private void OnCreateRecordOK(object sender, Record record)
76 | {
77 | UnityEngine.Debug.Log("SessionCreator: OnCreateRecordOK recordid " + record.Uuid);
78 | CreateRecordOK(this, record);
79 | }
80 |
81 | private void CreateSessionOk(object sender, SessionEventArgs sessionInfo)
82 | {
83 | lock(_locker) _sessionId = sessionInfo.SessionId;
84 |
85 | if (sessionInfo.Status == SessionStatus.Activated) {
86 | UnityEngine.Debug.Log("Session " + sessionInfo.SessionId + " is activated successfully.");
87 | SessionActived(this, sessionInfo);
88 | }
89 | else {
90 | UnityEngine.Debug.Log("Session " + sessionInfo.SessionId + " is opened successfully.");
91 | }
92 |
93 | }
94 | private void UpdateSessionOk(object sender, SessionEventArgs sessionInfo)
95 | {
96 |
97 | if (sessionInfo.Status == SessionStatus.Closed)
98 | {
99 | lock(_locker) _sessionId = "";
100 | SessionClosedOK(this, sessionInfo.SessionId);
101 |
102 | }
103 | else if (sessionInfo.Status == SessionStatus.Activated)
104 | {
105 | lock(_locker) _sessionId = sessionInfo.SessionId;
106 | SessionActived(this, sessionInfo);
107 | }
108 | }
109 |
110 | ///
111 | /// Open a session with an EMOTIV headset.
112 | /// A application can open only one session at a time with a given headset.
113 | ///
114 | public void Create(string cortexToken, string headsetId, bool activeSession = false)
115 | {
116 | if (!String.IsNullOrEmpty(cortexToken) &&
117 | !String.IsNullOrEmpty(headsetId))
118 | {
119 | string status = activeSession ? "active" : "open";
120 | _ctxClient.CreateSession(cortexToken, headsetId, status);
121 | }
122 | else {
123 | UnityEngine.Debug.Log("CreateSession: Invalid parameters");
124 | }
125 |
126 | }
127 |
128 | ///
129 | /// Close the current session.
130 | ///
131 | public void CloseSession(string cortexToken)
132 | {
133 | lock(_locker)
134 | {
135 | if (!String.IsNullOrEmpty(_sessionId)) {
136 | _ctxClient.UpdateSession(cortexToken, _sessionId, "close");
137 | }
138 | }
139 |
140 | }
141 |
142 | ///
143 | /// Create a new record.
144 | ///
145 | public void StartRecord(string cortexToken, string title,
146 | string description = null, string subjectName = null, List tags= null)
147 | {
148 | lock(_locker)
149 | {
150 | if (!String.IsNullOrEmpty(_sessionId)) {
151 | _ctxClient.CreateRecord(cortexToken, _sessionId, title, description, subjectName, tags);
152 | }
153 | else
154 | {
155 | UnityEngine.Debug.Log("StartRecord: invalid sessionId.");
156 | }
157 | }
158 |
159 | }
160 |
161 | ///
162 | /// Stop a record that was previously started by createRecord
163 | ///
164 | public void StopRecord(string cortexToken)
165 | {
166 | lock(_locker)
167 | {
168 | if (!String.IsNullOrEmpty(_sessionId)) {
169 | _ctxClient.StopRecord(cortexToken, _sessionId);
170 | }
171 | else
172 | {
173 | UnityEngine.Debug.Log("StopRecord: invalid sessionId.");
174 | }
175 | }
176 | }
177 |
178 | ///
179 | /// Update a record.
180 | ///
181 | public void UpdateRecord(string cortexToken, string recordId, string title = null,
182 | string description = null, List tags = null)
183 | {
184 | lock(_locker)
185 | {
186 | if (!String.IsNullOrEmpty(_sessionId)) {
187 | _ctxClient.UpdateRecord(cortexToken, recordId, title, description, tags);
188 | }
189 | else
190 | {
191 | UnityEngine.Debug.Log("StartRecord: invalid sessionId.");
192 | }
193 | }
194 | }
195 | // inject marker
196 |
197 |
198 | ///
199 | /// Clear current session Data.
200 | ///
201 | public void ClearSessionData() {
202 | lock(_locker)
203 | {
204 | _sessionId = "";
205 | }
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Src/SuperSocket.ClientEngine.Core.0.10.0/SuperSocket.ClientEngine.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/SuperSocket.ClientEngine.Core.0.10.0/SuperSocket.ClientEngine.dll
--------------------------------------------------------------------------------
/Src/UniWebViewManager.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_ANDROID || UNITY_IOS
2 | using UnityEngine;
3 | using System;
4 |
5 | public class UniWebViewManager : MonoBehaviour
6 | {
7 | private static readonly string LogTag = "[UniWebViewManager]"; // Log tag for consistency
8 |
9 | private static UniWebViewManager _instance;
10 | public static UniWebViewManager Instance
11 | {
12 | get
13 | {
14 | if (_instance == null)
15 | {
16 | var obj = new GameObject("UniWebViewManager");
17 | _instance = obj.AddComponent();
18 | DontDestroyOnLoad(obj);
19 | }
20 | return _instance;
21 | }
22 | }
23 |
24 | private UniWebViewAuthenticationSession authSession;
25 | private UniWebViewSafeBrowsing safeBrowsing;
26 | private string _authUrl;
27 | private string _urlScheme;
28 |
29 | public void Init(string authUrl, string urlScheme)
30 | {
31 | _authUrl = authUrl;
32 | _urlScheme = urlScheme;
33 | }
34 |
35 | public void StartAuthorization(Action onSuccess, Action onError)
36 | {
37 | if (string.IsNullOrEmpty(_authUrl) || string.IsNullOrEmpty(_urlScheme))
38 | {
39 | Debug.LogError($"{LogTag} Init must be called with valid authUrl and urlScheme before starting authorization.");
40 | return;
41 | }
42 |
43 | Debug.Log($"{LogTag} Starting authorization using UniWebViewAuthenticationSession...");
44 |
45 | authSession = UniWebViewAuthenticationSession.Create(_authUrl, _urlScheme);
46 |
47 | authSession.OnAuthenticationFinished += (session, result) =>
48 | {
49 | if (!string.IsNullOrEmpty(result))
50 | {
51 | Debug.Log($"{LogTag} Auth finished. Callback URL: {result}");
52 |
53 | string code = ExtractCodeFromUri(result);
54 | if (!string.IsNullOrEmpty(code))
55 | {
56 | onSuccess?.Invoke(code);
57 | }
58 | else
59 | {
60 | onError?.Invoke(-1, $"{LogTag} Authorization code not found in redirect URL.");
61 | }
62 | }
63 | else
64 | {
65 | onError?.Invoke(-2, $"{LogTag} Authentication session failed or was cancelled.");
66 | }
67 | };
68 |
69 | authSession.OnAuthenticationErrorReceived += (session, errorCode, errorMessage) =>
70 | {
71 | Debug.LogError($"{LogTag} Authentication Error: {errorCode} - {errorMessage}");
72 | onError?.Invoke(errorCode, errorMessage);
73 | };
74 |
75 | authSession.Start();
76 | }
77 |
78 | private string ExtractCodeFromUri(string uri)
79 | {
80 | try
81 | {
82 | var query = new Uri(uri).Query.TrimStart('?');
83 | foreach (var param in query.Split('&'))
84 | {
85 | var keyValue = param.Split('=');
86 | if (keyValue.Length == 2 && Uri.UnescapeDataString(keyValue[0]) == "code")
87 | {
88 | return Uri.UnescapeDataString(keyValue[1]);
89 | }
90 | }
91 | }
92 | catch (Exception e)
93 | {
94 | Debug.LogError($"{LogTag} Failed to extract code from URI: {e.Message}");
95 | }
96 |
97 | return null;
98 | }
99 |
100 | public void OpenURL(string url, ActiononClosed)
101 | {
102 | if (string.IsNullOrEmpty(url))
103 | {
104 | Debug.LogError($"{LogTag} URL cannot be null or empty.");
105 | return;
106 | }
107 |
108 | safeBrowsing = UniWebViewSafeBrowsing.Create(url);
109 | safeBrowsing.OnSafeBrowsingFinished += (browsing) => {
110 | Debug.Log("UniWebViewSafeBrowsing closed.");
111 | onClosed?.Invoke(true);
112 | };
113 |
114 | safeBrowsing.Show();
115 | }
116 |
117 | public void Cleanup()
118 | {
119 | if (authSession != null)
120 | {
121 | authSession = null;
122 | }
123 |
124 | _authUrl = null;
125 | _urlScheme = null;
126 | }
127 | }
128 | #endif
--------------------------------------------------------------------------------
/Src/Utils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using UnityEngine;
5 |
6 | namespace EmotivUnityPlugin
7 | {
8 | public static class Utils
9 | {
10 |
11 | public static Int64 GetEpochTimeNow()
12 | {
13 | TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1);
14 | Int64 timeSinceEpoch = (Int64)t.TotalMilliseconds;
15 | return timeSinceEpoch;
16 |
17 | }
18 | public static string GenerateUuidProfileName(string prefix)
19 | {
20 | return prefix + "-" + GetEpochTimeNow();
21 | }
22 |
23 | public static string GetAppTmpPath(string providerName, string appName)
24 | {
25 | string homePath = "";
26 | #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
27 | homePath = Environment.GetEnvironmentVariable("LocalAppData");
28 | #elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
29 | homePath = Environment.GetEnvironmentVariable("HOME");
30 | string currentTempPath = Path.Combine(homePath, "Library/Application Support");
31 | homePath = currentTempPath;
32 | #elif UNITY_STANDALONE_LINUX
33 | homePath = Environment.GetEnvironmentVariable("HOME");
34 | #elif UNITY_IOS
35 | homePath = Application.persistentDataPath;
36 | #elif UNITY_ANDROID
37 | // return application data path on android
38 | return Application.persistentDataPath;
39 | #else
40 | homePath = Directory.GetCurrentDirectory();
41 | #endif
42 | string targetFolderName = appName;
43 | if (!string.IsNullOrEmpty(providerName))
44 | targetFolderName = Path.Combine(providerName, appName);
45 |
46 | return Path.Combine(homePath, targetFolderName);
47 | }
48 |
49 | public static DateTime StringToIsoDateTime(string time) {
50 | // UnityEngine.Debug.Log(" StringToIsoDateTime: " + time);
51 | return DateTime.Parse(time);
52 | }
53 | public static string ISODateTimeToString(DateTime isoTime) {
54 | if (isoTime.CompareTo(new DateTime()) == 0)
55 | return "";
56 | return isoTime.ToString("yyyy-MM-ddTHH:mm:ss.ffffzzz");
57 | }
58 |
59 | public static double ISODateTimeToEpocTime(DateTime isoTime) {
60 | DateTime dt1970 = new DateTime(1970, 1, 1);
61 | TimeSpan span = isoTime - dt1970;
62 | return span.TotalMilliseconds;
63 | }
64 |
65 | public static bool CheckEmotivAppInstalled(string emotivAppsPath = "", bool isRequired = false) {
66 |
67 | if (!isRequired)
68 | return true;
69 |
70 | if (string.IsNullOrEmpty(emotivAppsPath)) {
71 | UnityEngine.Debug.Log("The emotivAppsPath is empty. So will not check emotiv installed or not.");
72 | return true;
73 | }
74 | else if (!Directory.Exists(emotivAppsPath)) {
75 | UnityEngine.Debug.Log("The emotivApps directory is not existed.");
76 | return false;
77 | }
78 |
79 |
80 | #if UNITY_STANDALONE_WIN
81 | string emotivAppName = "EMOTIV Launcher.exe";
82 | string fileDir = Path.Combine(emotivAppsPath, emotivAppName);
83 | if (File.Exists(fileDir)) {
84 | return true;
85 | }
86 | UnityEngine.Debug.Log("IsEmotivAppInstalled: not exists file: " + fileDir);
87 | return false;
88 | #elif UNITY_STANDALONE_OSX
89 | string emotivAppName = "EMOTIV Launcher.app";
90 | string appDir = Path.Combine(emotivAppsPath, emotivAppName);
91 | if (Directory.Exists(appDir)) {
92 | return true;
93 | }
94 | UnityEngine.Debug.Log("IsEmotivAppInstalled: not exists bundle file: " + appDir);
95 | return false;
96 | #elif UNITY_STANDALONE_LINUX
97 | string homePath = Environment.GetEnvironmentVariable("HOME");
98 | return true;
99 | // TODO
100 | #elif UNITY_IOS
101 | // TODO
102 | return true;
103 | #elif UNITY_ANDROID
104 | // TODO
105 | return true;
106 | #else
107 | // TODO
108 | return true;
109 | #endif
110 |
111 | }
112 |
113 | public static bool IsNumericType(object o)
114 | {
115 | switch (Type.GetTypeCode(o.GetType()))
116 | {
117 | case TypeCode.Byte:
118 | case TypeCode.SByte:
119 | case TypeCode.UInt16:
120 | case TypeCode.UInt32:
121 | case TypeCode.UInt64:
122 | case TypeCode.Int16:
123 | case TypeCode.Int32:
124 | case TypeCode.Int64:
125 | case TypeCode.Decimal:
126 | case TypeCode.Double:
127 | case TypeCode.Single:
128 | return true;
129 | default:
130 | return false;
131 | }
132 | }
133 |
134 | public static HeadsetFamily GetHeadsetGroup(HeadsetTypes headsetType)
135 | {
136 | if (headsetType == HeadsetTypes.HEADSET_TYPE_INSIGHT || headsetType == HeadsetTypes.HEADSET_TYPE_INSIGHT2)
137 | return HeadsetFamily.INSIGHT;
138 | else if (headsetType == HeadsetTypes.HEADSET_TYPE_MN8 || headsetType == HeadsetTypes.HEADSET_TYPE_MW20)
139 | return HeadsetFamily.MN8;
140 | else
141 | return HeadsetFamily.EPOC;
142 | }
143 |
144 | public static bool IsInsightType(HeadsetTypes headsetType)
145 | {
146 | if (headsetType == HeadsetTypes.HEADSET_TYPE_INSIGHT || headsetType == HeadsetTypes.HEADSET_TYPE_INSIGHT2)
147 | return true;
148 | else
149 | return false;
150 | }
151 |
152 | public static TimeSpan IndexToTime(int index)
153 | {
154 | if (index < 0 || index >= 48)
155 | {
156 | throw new ArgumentOutOfRangeException(nameof(index), "Index must be between 0 and 47.");
157 | }
158 |
159 | int hours = index / 2;
160 | int minutes = (index % 2) * 30;
161 |
162 | return new TimeSpan(hours, minutes, 0);
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/Src/WebSocket4Net.0.15.2/WebSocket4Net.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emotiv/unity-plugin/d243cc8c6f51b70599926cdd63515500ed7aaa50/Src/WebSocket4Net.0.15.2/WebSocket4Net.dll
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Plugins/iOS/ASWebAuthenticationSession.mm:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #include "Common.h"
4 |
5 | extern UIViewController* UnityGetGLViewController();
6 |
7 | typedef void (*ASWebAuthenticationSessionCompletionCallback)(void* sessionPtr, const char* callbackUrl, int errorCode, const char* errorMessage);
8 |
9 | @interface Cdm_ASWebAuthenticationSession : NSObject
10 |
11 | @property (readonly, nonatomic)ASWebAuthenticationSession* session;
12 |
13 | @end
14 |
15 | @implementation Cdm_ASWebAuthenticationSession
16 |
17 | - (instancetype)initWithURL:(NSURL *)URL callbackURLScheme:(nullable NSString *)callbackURLScheme completionCallback:(ASWebAuthenticationSessionCompletionCallback)completionCallback
18 | {
19 | _session = [[ASWebAuthenticationSession alloc] initWithURL:URL
20 | callbackURLScheme: callbackURLScheme
21 | completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error)
22 | {
23 | if (error != nil)
24 | {
25 | NSLog(@"[ASWebAuthenticationSession:CompletionHandler] %@", error.description);
26 | }
27 | else
28 | {
29 | //NSLog(@"[ASWebAuthenticationSession:CompletionHandler] Callback URL: %@", callbackURL);
30 | }
31 |
32 | completionCallback((__bridge void*)self, toString(callbackURL.absoluteString), (int)error.code, toString(error.localizedDescription));
33 | }];
34 |
35 | [_session setPresentationContextProvider:self];
36 | return self;
37 | }
38 |
39 | - (nonnull ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(nonnull ASWebAuthenticationSession *)session
40 | {
41 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 || __TV_OS_VERSION_MAX_ALLOWED >= 130000
42 | return [[[UIApplication sharedApplication] delegate] window];
43 | #elif __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500
44 | return [[NSApplication sharedApplication] mainWindow];
45 | #else
46 | return nil;
47 | #endif
48 | }
49 |
50 | @end
51 |
52 | extern "C"
53 | {
54 | Cdm_ASWebAuthenticationSession* Cdm_Auth_ASWebAuthenticationSession_InitWithURL(
55 | const char* urlStr, const char* urlSchemeStr, ASWebAuthenticationSessionCompletionCallback completionCallback)
56 | {
57 | //NSLog(@"[ASWebAuthenticationSession:InitWithURL] initWithURL: %s callbackURLScheme:%s", urlStr, urlSchemeStr);
58 |
59 | NSURL* url = [NSURL URLWithString: toString(urlStr)];
60 | NSString* urlScheme = toString(urlSchemeStr);
61 |
62 | Cdm_ASWebAuthenticationSession* session = [[Cdm_ASWebAuthenticationSession alloc] initWithURL:url
63 | callbackURLScheme: urlScheme
64 | completionCallback:completionCallback];
65 | return session;
66 | }
67 |
68 | // Starts a web authentication session.
69 | // https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/2990953-start?language=objc
70 | int Cdm_Auth_ASWebAuthenticationSession_Start(void* sessionPtr)
71 | {
72 | Cdm_ASWebAuthenticationSession* session = (__bridge Cdm_ASWebAuthenticationSession*) sessionPtr;
73 | BOOL started = [[session session] start];
74 |
75 | //NSLog(@"[ASWebAuthenticationSession:Start]: %s", (started ? "YES" : "NO"));
76 |
77 | return toBool(started);
78 | }
79 |
80 | // Cancels a web authentication session.
81 | // https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/2990951-cancel?language=objc
82 | void Cdm_Auth_ASWebAuthenticationSession_Cancel(void* sessionPtr)
83 | {
84 | //NSLog(@"[ASWebAuthenticationSession:Cancel]");
85 |
86 | Cdm_ASWebAuthenticationSession* session = (__bridge Cdm_ASWebAuthenticationSession*) sessionPtr;
87 | [[session session] cancel];
88 | }
89 |
90 | int Cdm_Auth_ASWebAuthenticationSession_GetPrefersEphemeralWebBrowserSession(void* sessionPtr)
91 | {
92 | Cdm_ASWebAuthenticationSession* session = (__bridge Cdm_ASWebAuthenticationSession*) sessionPtr;
93 | return toBool([[session session] prefersEphemeralWebBrowserSession]);
94 | }
95 |
96 | void Cdm_Auth_ASWebAuthenticationSession_SetPrefersEphemeralWebBrowserSession(void* sessionPtr, int enable)
97 | {
98 | Cdm_ASWebAuthenticationSession* session = (__bridge Cdm_ASWebAuthenticationSession*) sessionPtr;
99 | [[session session] setPrefersEphemeralWebBrowserSession:toBool(enable)];
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Plugins/iOS/Common.h:
--------------------------------------------------------------------------------
1 | #ifndef Common_h
2 | #define Common_h
3 |
4 | typedef int bool_t;
5 |
6 | inline bool_t toBool(bool v)
7 | {
8 | return v ? 1 : 0;
9 | }
10 |
11 | inline bool toBool(bool_t v)
12 | {
13 | return v != 0;
14 | }
15 |
16 | inline NSString* toString(const char* string)
17 | {
18 | if (string != NULL)
19 | {
20 | return [NSString stringWithUTF8String:string];
21 | }
22 | else
23 | {
24 | return [NSString stringWithUTF8String:""];
25 | }
26 | }
27 |
28 | inline char* toString(NSString* string)
29 | {
30 | const char* cstr = [string UTF8String];
31 |
32 | if (cstr == NULL)
33 | return NULL;
34 |
35 | char* copy = (char*)malloc(strlen(cstr) + 1);
36 | strcpy(copy, cstr);
37 | return copy;
38 | }
39 |
40 | #endif /* Common_h */
41 |
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/ASWebAuthenticationSession.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using AOT;
4 |
5 | #if UNITY_IOS && !UNITY_EDITOR
6 | using System.Runtime.InteropServices;
7 | #endif
8 |
9 | namespace Cdm.Authentication.Browser
10 | {
11 | ///
12 | /// A session that an app uses to authenticate a user through a web service.
13 | ///
14 | ///
15 | public class ASWebAuthenticationSession : IDisposable
16 | {
17 | private static readonly Dictionary CompletionCallbacks =
18 | new Dictionary();
19 |
20 | private IntPtr _sessionPtr;
21 |
22 | ///
23 | /// A Boolean value that indicates whether the session should ask the browser for a private authentication
24 | /// session.
25 | ///
26 | /// Set this property before you call . Otherwise it has no effect.
27 | public bool prefersEphemeralWebBrowserSession
28 | {
29 | get => Cdm_Auth_ASWebAuthenticationSession_GetPrefersEphemeralWebBrowserSession(_sessionPtr) == 1;
30 | set => Cdm_Auth_ASWebAuthenticationSession_SetPrefersEphemeralWebBrowserSession(_sessionPtr, value ? 1 : 0);
31 | }
32 |
33 | ///
34 | /// Creates a web authentication session instance.
35 | ///
36 | /// A URL with the http or https scheme pointing to the authentication webpage.
37 | /// The custom URL scheme that the app expects in the callback URL.
38 | /// A completion handler the session calls when it completes successfully, or when the user cancels the session.
39 | ///
40 | public ASWebAuthenticationSession(string url, string callbackUrlScheme,
41 | ASWebAuthenticationSessionCompletionHandler completionHandler)
42 | {
43 | _sessionPtr =
44 | Cdm_Auth_ASWebAuthenticationSession_InitWithURL(
45 | url, callbackUrlScheme, OnAuthenticationSessionCompleted);
46 |
47 | CompletionCallbacks.Add(_sessionPtr, completionHandler);
48 | }
49 |
50 | ///
51 | /// Starts a web authentication session.
52 | ///
53 | /// A Boolean value indicating whether the web authentication session started successfully.
54 | ///
55 | public bool Start()
56 | {
57 | return Cdm_Auth_ASWebAuthenticationSession_Start(_sessionPtr) == 1;
58 | }
59 |
60 | ///
61 | /// Cancels a web authentication session.
62 | ///
63 | ///
64 | public void Cancel()
65 | {
66 | Cdm_Auth_ASWebAuthenticationSession_Cancel(_sessionPtr);
67 | }
68 |
69 | public void Dispose()
70 | {
71 | CompletionCallbacks.Remove(_sessionPtr);
72 | _sessionPtr = IntPtr.Zero;
73 | }
74 |
75 | #if UNITY_IOS && !UNITY_EDITOR
76 | private const string DllName = "__Internal";
77 |
78 | [DllImport(DllName)]
79 | private static extern IntPtr Cdm_Auth_ASWebAuthenticationSession_InitWithURL(
80 | string url, string callbackUrlScheme, AuthenticationSessionCompletedCallback completionHandler);
81 |
82 | [DllImport(DllName)]
83 | private static extern int Cdm_Auth_ASWebAuthenticationSession_Start(IntPtr session);
84 |
85 | [DllImport(DllName)]
86 | private static extern void Cdm_Auth_ASWebAuthenticationSession_Cancel(IntPtr session);
87 |
88 | [DllImport(DllName)]
89 | private static extern int Cdm_Auth_ASWebAuthenticationSession_GetPrefersEphemeralWebBrowserSession(IntPtr session);
90 |
91 | [DllImport(DllName)]
92 | private static extern void Cdm_Auth_ASWebAuthenticationSession_SetPrefersEphemeralWebBrowserSession(
93 | IntPtr session, int enable);
94 | #else
95 |
96 | private const string NotSupportedMsg = "Only iOS platform is supported.";
97 |
98 | private static IntPtr Cdm_Auth_ASWebAuthenticationSession_InitWithURL(
99 | string url, string callbackUrlScheme, AuthenticationSessionCompletedCallback completionHandler)
100 | {
101 | throw new NotImplementedException(NotSupportedMsg);
102 | }
103 |
104 | private static int Cdm_Auth_ASWebAuthenticationSession_Start(IntPtr session)
105 | {
106 | throw new NotImplementedException(NotSupportedMsg);
107 | }
108 |
109 | private static void Cdm_Auth_ASWebAuthenticationSession_Cancel(IntPtr session)
110 | {
111 | throw new NotImplementedException(NotSupportedMsg);
112 | }
113 |
114 | private static int Cdm_Auth_ASWebAuthenticationSession_GetPrefersEphemeralWebBrowserSession(IntPtr session)
115 | {
116 | throw new NotImplementedException(NotSupportedMsg);
117 | }
118 |
119 | private static void Cdm_Auth_ASWebAuthenticationSession_SetPrefersEphemeralWebBrowserSession(
120 | IntPtr session, int enable)
121 | {
122 | throw new NotImplementedException(NotSupportedMsg);
123 | }
124 | #endif
125 | public delegate void ASWebAuthenticationSessionCompletionHandler(string callbackUrl,
126 | ASWebAuthenticationSessionError error);
127 |
128 | private delegate void AuthenticationSessionCompletedCallback(IntPtr session, string callbackUrl,
129 | int errorCode, string errorMessage);
130 |
131 | [MonoPInvokeCallback(typeof(AuthenticationSessionCompletedCallback))]
132 | private static void OnAuthenticationSessionCompleted(IntPtr session, string callbackUrl,
133 | int errorCode, string errorMessage)
134 | {
135 | if (CompletionCallbacks.TryGetValue(session, out var callback))
136 | {
137 | callback?.Invoke(callbackUrl,
138 | new ASWebAuthenticationSessionError((ASWebAuthenticationSessionErrorCode) errorCode, errorMessage));
139 | }
140 | }
141 | }
142 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/ASWebAuthenticationSessionBrowser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Cdm.Authentication.Browser
6 | {
7 | public class ASWebAuthenticationSessionBrowser : IBrowser
8 | {
9 | private TaskCompletionSource _taskCompletionSource;
10 |
11 | ///
12 | /// Indicates whether the session should ask the browser for a private authentication
13 | /// session.
14 | ///
15 | ///
16 | /// Set this property before you call . Otherwise it has no effect.
17 | ///
18 | public bool prefersEphemeralWebBrowserSession { get; set; } = false;
19 |
20 | public async Task StartAsync(
21 | string loginUrl, string redirectUrl, CancellationToken cancellationToken = default)
22 | {
23 | if (string.IsNullOrEmpty(loginUrl))
24 | throw new ArgumentNullException(nameof(loginUrl));
25 |
26 | if (string.IsNullOrEmpty(redirectUrl))
27 | throw new ArgumentNullException(nameof(redirectUrl));
28 |
29 | _taskCompletionSource = new TaskCompletionSource();
30 |
31 | // Discard URL parameters. They are not valid for iOS URL Scheme.
32 | redirectUrl = redirectUrl.Split(new char[] {':'}, StringSplitOptions.RemoveEmptyEntries)[0];
33 |
34 | using var authenticationSession =
35 | new ASWebAuthenticationSession(loginUrl, redirectUrl, AuthenticationSessionCompletionHandler);
36 | authenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession;
37 |
38 | cancellationToken.Register(() =>
39 | {
40 | _taskCompletionSource?.TrySetCanceled();
41 | });
42 |
43 | try
44 | {
45 | if (!authenticationSession.Start())
46 | {
47 | _taskCompletionSource.SetResult(
48 | new BrowserResult(BrowserStatus.UnknownError, "Browser could not be started."));
49 | }
50 |
51 | return await _taskCompletionSource.Task;
52 | }
53 | catch (TaskCanceledException)
54 | {
55 | // In case of timeout cancellation.
56 | authenticationSession?.Cancel();
57 | throw;
58 | }
59 | }
60 |
61 | private void AuthenticationSessionCompletionHandler(string callbackUrl, ASWebAuthenticationSessionError error)
62 | {
63 | if (error.code == ASWebAuthenticationSessionErrorCode.None)
64 | {
65 | _taskCompletionSource.SetResult(
66 | new BrowserResult(BrowserStatus.Success, callbackUrl));
67 | }
68 | else if (error.code == ASWebAuthenticationSessionErrorCode.CanceledLogin)
69 | {
70 | _taskCompletionSource.SetResult(
71 | new BrowserResult(BrowserStatus.UserCanceled, callbackUrl, error.message));
72 | }
73 | else
74 | {
75 | _taskCompletionSource.SetResult(
76 | new BrowserResult(BrowserStatus.UnknownError, callbackUrl, error.message));
77 | }
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/ASWebAuthenticationSessionError.cs:
--------------------------------------------------------------------------------
1 | namespace Cdm.Authentication.Browser
2 | {
3 | public class ASWebAuthenticationSessionError
4 | {
5 | public ASWebAuthenticationSessionErrorCode code { get; }
6 | public string message { get; }
7 |
8 | public ASWebAuthenticationSessionError(ASWebAuthenticationSessionErrorCode code, string message)
9 | {
10 | this.code = code;
11 | this.message = message;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/ASWebAuthenticationSessionErrorCode.cs:
--------------------------------------------------------------------------------
1 | namespace Cdm.Authentication.Browser
2 | {
3 | public enum ASWebAuthenticationSessionErrorCode
4 | {
5 | None = 0,
6 | CanceledLogin = 1,
7 | PresentationContextNotProvided = 2,
8 | PresentationContextInvalid = 3
9 | }
10 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/BrowserResult.cs:
--------------------------------------------------------------------------------
1 | namespace Cdm.Authentication.Browser
2 | {
3 | public class BrowserResult
4 | {
5 | ///
6 | /// The browser status indicates the operation is whether success or not.
7 | ///
8 | public BrowserStatus status { get; }
9 |
10 | ///
11 | /// After a user successfully authorizes an application, the authorization server will redirect the user back
12 | /// to the application with the redirect URL. Use this if only if is
13 | /// .
14 | ///
15 | public string redirectUrl { get; }
16 |
17 | ///
18 | /// The error description if an error is exist. You can use this value if is not
19 | /// .
20 | ///
21 | public string error { get; }
22 |
23 | public BrowserResult(BrowserStatus status, string redirectUrl)
24 | {
25 | this.status = status;
26 | this.redirectUrl = redirectUrl;
27 | }
28 |
29 | public BrowserResult(BrowserStatus status, string redirectUrl, string error)
30 | {
31 | this.status = status;
32 | this.redirectUrl = redirectUrl;
33 | this.error = error;
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/BrowserStatus.cs:
--------------------------------------------------------------------------------
1 | namespace Cdm.Authentication.Browser
2 | {
3 | public enum BrowserStatus
4 | {
5 | Success,
6 | UserCanceled,
7 | UnknownError,
8 | }
9 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/CallbackManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipes;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace Cdm.Authentication.Browser
8 | {
9 | public class CallbackManager
10 | {
11 | private readonly string _name;
12 |
13 | public CallbackManager(string name)
14 | {
15 | _name = name ?? throw new ArgumentNullException(nameof(name));
16 | }
17 |
18 | public int ClientConnectTimeoutSeconds { get; set; } = 1;
19 |
20 | public async Task RunClient(string args)
21 | {
22 | using (var client = new NamedPipeClientStream(".", _name, PipeDirection.Out))
23 | {
24 | await client.ConnectAsync(ClientConnectTimeoutSeconds * 1000);
25 |
26 | using (var sw = new StreamWriter(client) { AutoFlush = true })
27 | {
28 | await sw.WriteAsync(args);
29 | }
30 | }
31 | }
32 |
33 | public async Task RunServer(CancellationToken? token = null)
34 | {
35 | token = CancellationToken.None;
36 |
37 | using (var server = new NamedPipeServerStream(_name, PipeDirection.In))
38 | {
39 | await server.WaitForConnectionAsync(token.Value);
40 |
41 | using (var sr = new StreamReader(server))
42 | {
43 | var msg = await sr.ReadToEndAsync();
44 | return msg;
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/CrossPlatformBrowser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using UnityEngine;
7 |
8 | namespace Cdm.Authentication.Browser
9 | {
10 | public class CrossPlatformBrowser : IBrowser
11 | {
12 | public readonly Dictionary _platformBrowsers =
13 | new Dictionary();
14 |
15 | public IDictionary platformBrowsers => _platformBrowsers;
16 |
17 | public async Task StartAsync(
18 | string loginUrl, string redirectUrl, CancellationToken cancellationToken = default)
19 | {
20 | var browser = platformBrowsers.FirstOrDefault(x => x.Key == Application.platform).Value;
21 | if (browser == null)
22 | throw new NotSupportedException($"There is no browser found for '{Application.platform}' platform.");
23 |
24 | return await browser.StartAsync(loginUrl, redirectUrl, cancellationToken);
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/DeepLinkBrowser.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using UnityEngine;
4 |
5 | namespace Cdm.Authentication.Browser
6 | {
7 | ///
8 | /// OAuth 2.0 verification browser that waits for a call with
9 | /// the authorization verification code through a custom scheme (aka protocol).
10 | ///
11 | ///
12 | public class DeepLinkBrowser : IBrowser
13 | {
14 | private TaskCompletionSource _taskCompletionSource;
15 |
16 | public async Task StartAsync(string loginUrl, string redirectUrl, CancellationToken cancellationToken = default)
17 | {
18 | _taskCompletionSource = new TaskCompletionSource();
19 |
20 | cancellationToken.Register(() =>
21 | {
22 | _taskCompletionSource?.TrySetCanceled();
23 | });
24 |
25 | Application.deepLinkActivated += OnDeepLinkActivated;
26 | try
27 | {
28 | Application.OpenURL(loginUrl);
29 | return await _taskCompletionSource.Task;
30 | }
31 | finally
32 | {
33 | Application.deepLinkActivated -= OnDeepLinkActivated;
34 | }
35 | }
36 |
37 | private void OnDeepLinkActivated(string url)
38 | {
39 | _taskCompletionSource.SetResult(
40 | new BrowserResult(BrowserStatus.Success, url));
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/IBrowser.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Cdm.Authentication.Browser
5 | {
6 | public interface IBrowser
7 | {
8 | Task StartAsync(
9 | string loginUrl, string redirectUrl, CancellationToken cancellationToken = default);
10 | }
11 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/StandaloneBrowser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using UnityEngine;
6 |
7 | namespace Cdm.Authentication.Browser
8 | {
9 | ///
10 | /// OAuth 2.0 verification browser that runs a local server and waits for a call with
11 | /// the authorization verification code.
12 | ///
13 | public class StandaloneBrowser : IBrowser
14 | {
15 | private TaskCompletionSource _taskCompletionSource;
16 |
17 | ///
18 | /// Gets or sets the close page response. This HTML response is shown to the user after redirection is done.
19 | ///
20 | public string closePageResponse { get; set; } =
21 | "DONE!
(You can close this tab/window now)";
22 |
23 | public async Task StartAsync(
24 | string loginUrl, string redirectUrl, CancellationToken cancellationToken = default)
25 | {
26 | _taskCompletionSource = new TaskCompletionSource();
27 |
28 | cancellationToken.Register(() =>
29 | {
30 | _taskCompletionSource?.TrySetCanceled();
31 | });
32 |
33 | using var httpListener = new HttpListener();
34 |
35 | try
36 | {
37 |
38 | redirectUrl = AddForwardSlashIfNecessary(redirectUrl);
39 | httpListener.Prefixes.Add(redirectUrl);
40 | httpListener.Start();
41 | httpListener.BeginGetContext(IncomingHttpRequest, httpListener);
42 |
43 | Application.OpenURL(loginUrl);
44 |
45 | return await _taskCompletionSource.Task;
46 | }
47 | finally
48 | {
49 | httpListener.Stop();
50 | }
51 | }
52 |
53 | private void IncomingHttpRequest(IAsyncResult result)
54 | {
55 | var httpListener = (HttpListener)result.AsyncState;
56 | var httpContext = httpListener.EndGetContext(result);
57 | var httpRequest = httpContext.Request;
58 |
59 | // Build a response to send an "ok" back to the browser for the user to see.
60 | var httpResponse = httpContext.Response;
61 | var buffer = System.Text.Encoding.UTF8.GetBytes(closePageResponse);
62 |
63 | // Send the output to the client browser.
64 | httpResponse.ContentLength64 = buffer.Length;
65 | var output = httpResponse.OutputStream;
66 | output.Write(buffer, 0, buffer.Length);
67 | output.Close();
68 |
69 | _taskCompletionSource.SetResult(
70 | new BrowserResult(BrowserStatus.Success, httpRequest.Url.ToString()));
71 | }
72 |
73 | ///
74 | /// Prefixes must end in a forward slash ("/")
75 | ///
76 | ///
77 | private string AddForwardSlashIfNecessary(string url)
78 | {
79 | string forwardSlash = "/";
80 | if (!url.EndsWith(forwardSlash))
81 | {
82 | url += forwardSlash;
83 | }
84 |
85 | return url;
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Browser/WindowsSystemBrowser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using System.Web;
5 | #if USE_EMBEDDED_LIB && (UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
6 | using IdentityModel.Client;
7 | #endif
8 | using UnityEngine;
9 |
10 | namespace Cdm.Authentication.Browser
11 | {
12 | public class WindowsSystemBrowser : IBrowser
13 | {
14 | private TaskCompletionSource _taskCompletionSource;
15 | public async Task StartAsync(
16 | string loginUrl, string redirectUrl, CancellationToken cancellationToken = default)
17 | {
18 | if (string.IsNullOrEmpty(loginUrl))
19 | throw new ArgumentNullException(nameof(loginUrl));
20 |
21 | if (string.IsNullOrEmpty(redirectUrl))
22 | throw new ArgumentNullException(nameof(redirectUrl));
23 |
24 | _taskCompletionSource = new TaskCompletionSource();
25 | cancellationToken.Register(() => { _taskCompletionSource?.TrySetCanceled(); });
26 |
27 | try
28 | {
29 | var state = ExtractStateFromUrl(loginUrl);
30 | Debug.Log("Opening browser for login: " + loginUrl + " with state: " + state);
31 | var callbackManager = new CallbackManager(state);
32 |
33 | Application.OpenURL(loginUrl);
34 | var response = await callbackManager.RunServer();
35 | // check response is not null
36 | if (response == null)
37 | {
38 | _taskCompletionSource.SetResult(
39 | new BrowserResult(BrowserStatus.UnknownError, "Browser could not be started."));
40 | return await _taskCompletionSource.Task;
41 | }
42 | if (response == "error")
43 | {
44 | _taskCompletionSource.SetResult(
45 | new BrowserResult(BrowserStatus.UserCanceled, "User canceled the login."));
46 | return await _taskCompletionSource.Task;
47 | }
48 | _taskCompletionSource.SetResult(
49 | new BrowserResult(BrowserStatus.Success, response));
50 |
51 | return await _taskCompletionSource.Task;
52 | }
53 | catch (Exception ex)
54 | {
55 | _taskCompletionSource.SetResult(
56 | new BrowserResult(BrowserStatus.UnknownError, ex.Message));
57 | return await _taskCompletionSource.Task;
58 | }
59 |
60 |
61 | }
62 | public string ExtractStateFromUrl(string url)
63 | {
64 | Uri uri = new Uri(url);
65 | string query = uri.Query;
66 | var queryParams = HttpUtility.ParseQueryString(query);
67 | return queryParams["state"];
68 | }
69 |
70 | public static async Task ProcessCallback(string args)
71 | {
72 | UnityEngine.Debug.Log("Processing callback" + args);
73 | #if USE_EMBEDDED_LIB && (UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
74 | var response = new AuthorizeResponse(args);
75 | if (!String.IsNullOrWhiteSpace(response.State))
76 | {
77 | UnityEngine.Debug.Log($"Found state: {response.State}");
78 | var callbackManager = new CallbackManager(response.State);
79 | await callbackManager.RunClient(args);
80 | await Task.Delay(1000);
81 | Application.Quit();
82 | }
83 | else
84 | {
85 | UnityEngine.Debug.Log("Error: no state on response");
86 | }
87 | #endif
88 | }
89 | }
90 |
91 |
92 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Cdm.Authentication.asmdef:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Cdm.Authentication",
3 | "rootNamespace": "Cdm.Authentication",
4 | "references": [],
5 | "includePlatforms": [],
6 | "excludePlatforms": [],
7 | "allowUnsafeCode": false,
8 | "overrideReferences": false,
9 | "precompiledReferences": [],
10 | "autoReferenced": true,
11 | "defineConstraints": [
12 | "UNITY_2019_1_OR_NEWER"
13 | ],
14 | "versionDefines": [],
15 | "noEngineReferences": false
16 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Clients/MockServerAuth.cs:
--------------------------------------------------------------------------------
1 | using Cdm.Authentication.OAuth2;
2 |
3 | namespace Cdm.Authentication.Clients
4 | {
5 | public class MockServerAuth : AuthorizationCodeFlow
6 | {
7 | public const string AuthorizationPath = "/api/oauth/authorize/";
8 | public const string TokenPath = "/api/oauth/token/oidc/";
9 |
10 | public override string authorizationUrl => $"{serverUrl}{AuthorizationPath}";
11 | public override string accessTokenUrl => $"{serverUrl}{TokenPath}";
12 |
13 | public string serverUrl { get; }
14 |
15 | public MockServerAuth(Configuration configuration, string serverUrl) : base(configuration)
16 | {
17 | this.serverUrl = serverUrl;
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/IUserInfo.cs:
--------------------------------------------------------------------------------
1 | namespace Cdm.Authentication
2 | {
3 | public interface IUserInfo
4 | {
5 | ///
6 | /// Gets the user identifier.
7 | ///
8 | public string id { get; }
9 |
10 | ///
11 | /// Gets the name of the user.
12 | ///
13 | public string name { get; }
14 |
15 | ///
16 | /// Gets the email address of the user.
17 | ///
18 | public string email { get; }
19 |
20 | ///
21 | /// Gets the user picture URL.
22 | ///
23 | public string picture { get; }
24 | }
25 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/IUserInfoProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Cdm.Authentication
5 | {
6 | public interface IUserInfoProvider
7 | {
8 | ///
9 | /// Obtains user information using third-party authentication service using data provided via callback request.
10 | ///
11 | /// Optional cancellation token.
12 | Task GetUserInfoAsync(CancellationToken cancellationToken = default);
13 | }
14 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AccessTokenRequest.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using UnityEngine.Scripting;
3 |
4 | namespace Cdm.Authentication.OAuth2
5 | {
6 | ///
7 | /// OAuth 2.0 request for an access token using an authorization code as specified in
8 | /// http://tools.ietf.org/html/rfc6749#section-4.1.3.
9 | ///
10 | [Preserve]
11 | [DataContract]
12 | public class AccessTokenRequest
13 | {
14 | ///
15 | /// Gets the authorization grant type as 'authorization_code'.
16 | ///
17 | [Preserve]
18 | [DataMember(IsRequired = true, Name = "grant_type")]
19 | public string grantType => "authorization_code";
20 |
21 | ///
22 | /// Gets or sets the authorization code received from the authorization server.
23 | ///
24 | [Preserve]
25 | [DataMember(IsRequired = true, Name = "code")]
26 | public string code { get; set; }
27 |
28 | ///
29 | /// Gets or sets the client identifier as described in https://www.rfc-editor.org/rfc/rfc6749#section-3.2.1.
30 | ///
31 | [Preserve]
32 | [DataMember(IsRequired = true, Name = "client_id")]
33 | public string clientId { get; set; }
34 |
35 | ///
36 | /// Gets or sets the client secret.
37 | ///
38 | [Preserve]
39 | [DataMember(Name = "client_secret")]
40 | public string clientSecret { get; set; }
41 |
42 | ///
43 | /// Gets or sets the redirect URI parameter matching the redirect URI parameter in the authorization request.
44 | ///
45 | [Preserve]
46 | [DataMember(Name = "redirect_uri")]
47 | public string redirectUri { get; set; }
48 | }
49 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AccessTokenRequestError.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using UnityEngine.Scripting;
3 |
4 | namespace Cdm.Authentication.OAuth2
5 | {
6 | ///
7 | /// OAuth 2.0 model for a unsuccessful access token response as specified in
8 | /// http://tools.ietf.org/html/rfc6749#section-5.2.
9 | ///
10 | [Preserve]
11 | [DataContract]
12 | public class AccessTokenRequestError
13 | {
14 | ///
15 | /// Gets or sets the error code as specified in http://tools.ietf.org/html/rfc6749#section-5.2.
16 | ///
17 | [Preserve]
18 | [DataMember(IsRequired = true, Name = "error")]
19 | public AccessTokenRequestErrorCode code { get; set; }
20 |
21 | ///
22 | /// Gets or sets a human-readable text which provides additional information used to assist the client
23 | /// developer in understanding the error occurred.
24 | ///
25 | [Preserve]
26 | [DataMember(Name = "error_description")]
27 | public string description { get; set; }
28 |
29 | ///
30 | /// Gets or sets the URI identifying a human-readable web page with provides information about the error.
31 | ///
32 | [Preserve]
33 | [DataMember(Name = "error_uri")]
34 | public string uri { get; set; }
35 | }
36 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AccessTokenRequestErrorCode.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using Newtonsoft.Json;
3 | using Newtonsoft.Json.Converters;
4 | using UnityEngine.Scripting;
5 |
6 | namespace Cdm.Authentication.OAuth2
7 | {
8 | ///
9 | /// The authorization server responds with an HTTP 400 (Bad Request) status code (unless specified otherwise) and
10 | /// includes the following parameters with the response.
11 | ///
12 | [JsonConverter(typeof(StringEnumConverter))]
13 | [DataContract]
14 | [Preserve]
15 | public enum AccessTokenRequestErrorCode
16 | {
17 | ///
18 | /// The request is missing a required parameter, includes an unsupported parameter value
19 | /// (other than grant type), repeats a parameter, includes multiple credentials, utilizes more than
20 | /// one mechanism for authenticating the client, or is otherwise malformed.
21 | ///
22 | [Preserve]
23 | [EnumMember(Value = "invalid_request")]
24 | InvalidRequest,
25 |
26 | ///
27 | /// Client authentication failed (e.g., unknown client, no client authentication included,
28 | /// or unsupported authentication method). The authorization server MAY return an HTTP 401 (Unauthorized)
29 | /// status code to indicate which HTTP authentication schemes are supported. If the client attempted to
30 | /// authenticate via the "Authorization" request header field, the authorization server MUST respond with
31 | /// an HTTP 401 (Unauthorized) status code and include the "WWW-Authenticate" response header field matching
32 | /// the authentication scheme used by the client.
33 | ///
34 | [Preserve]
35 | [EnumMember(Value = "invalid_client")]
36 | InvalidClient,
37 |
38 | ///
39 | /// The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token
40 | /// is invalid, expired, revoked, does not match the redirection URI used in the authorization request,
41 | /// or was issued to another client.
42 | ///
43 | [Preserve]
44 | [EnumMember(Value = "invalid_grant")]
45 | InvalidGrant,
46 |
47 | ///
48 | /// The authenticated client is not authorized to use this authorization grant type.
49 | ///
50 | [Preserve]
51 | [EnumMember(Value = "unauthorized_client")]
52 | UnauthorizedClient,
53 |
54 | ///
55 | /// The authorization grant type is not supported by the authorization server.
56 | ///
57 | [Preserve]
58 | [EnumMember(Value = "unsupported_grant_type")]
59 | UnsupportedGrantType,
60 |
61 | ///
62 | /// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.
63 | ///
64 | [Preserve]
65 | [EnumMember(Value = "invalid_scope")]
66 | InvalidScope,
67 | }
68 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AccessTokenRequestException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 |
4 | namespace Cdm.Authentication.OAuth2
5 | {
6 | ///
7 | /// Access token response exception which is thrown in case of receiving a token error when an authorization code
8 | /// or an access token is expected.
9 | ///
10 | public class AccessTokenRequestException : Exception
11 | {
12 | ///
13 | /// HTTP status code of error, or null if unknown.
14 | ///
15 | public HttpStatusCode? statusCode { get; }
16 |
17 | ///
18 | /// The error information.
19 | ///
20 | public AccessTokenRequestError error { get; }
21 |
22 | public AccessTokenRequestException(AccessTokenRequestError error, HttpStatusCode? statusCode)
23 | : base(error.description)
24 | {
25 | this.error = error;
26 | this.statusCode = statusCode;
27 | }
28 |
29 | public AccessTokenRequestException(AccessTokenRequestError error, HttpStatusCode? statusCode, string message)
30 | : base(message)
31 | {
32 | this.error = error;
33 | this.statusCode = statusCode;
34 | }
35 |
36 | public AccessTokenRequestException(AccessTokenRequestError error, HttpStatusCode? statusCode,
37 | string message, Exception innerException) : base(message, innerException)
38 | {
39 | this.error = error;
40 | this.statusCode = statusCode;
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AccessTokenResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http.Headers;
3 | using System.Runtime.Serialization;
4 | using UnityEngine.Scripting;
5 |
6 | namespace Cdm.Authentication.OAuth2
7 | {
8 | [Preserve]
9 | [DataContract]
10 | public class AccessTokenResponse
11 | {
12 | ///
13 | /// Gets or sets the access token issued by the authorization server.
14 | ///
15 | [Preserve]
16 | [DataMember(IsRequired = true, Name = "access_token")]
17 | public string accessToken { get; set; }
18 |
19 | ///
20 | /// Gets or sets the refresh token which can be used to obtain a new access token.
21 | ///
22 | [Preserve]
23 | [DataMember(Name = "refresh_token")]
24 | public string refreshToken { get; set; }
25 |
26 | ///
27 | /// Gets or sets the token type as specified in http://tools.ietf.org/html/rfc6749#section-7.1.
28 | ///
29 | [Preserve]
30 | [DataMember(IsRequired = true, Name = "token_type")]
31 | public string tokenType { get; set; }
32 |
33 | ///
34 | /// Gets or sets the lifetime in seconds of the access token.
35 | ///
36 | [Preserve]
37 | [DataMember(Name = "expires_in")]
38 | public long? expiresIn { get; set; }
39 |
40 | ///
41 | /// Gets or sets the scope of the access token as specified in http://tools.ietf.org/html/rfc6749#section-3.3.
42 | ///
43 | [Preserve]
44 | [DataMember(Name = "scope")]
45 | public string scope { get; set; }
46 |
47 | ///
48 | /// The date and time that this token was issued, expressed in UTC.
49 | ///
50 | ///
51 | /// This should be set by the client after the token was received from the server.
52 | ///
53 | public DateTime? issuedAt { get; set; }
54 |
55 | ///
56 | /// Seconds till the expires returned by provider.
57 | ///
58 | public DateTime? expiresAt
59 | {
60 | get
61 | {
62 | if (issuedAt.HasValue && expiresIn.HasValue)
63 | {
64 | return issuedAt.Value + TimeSpan.FromSeconds(expiresIn.Value);
65 | }
66 |
67 | return null;
68 | }
69 | }
70 |
71 | public AuthenticationHeaderValue GetAuthenticationHeader()
72 | {
73 | return new AuthenticationHeaderValue(tokenType, accessToken);
74 | }
75 |
76 | ///
77 | /// Returns true if the token is expired or it's going to expire soon.
78 | ///
79 | ///
80 | /// If a token response does not have then it's considered expired.
81 | /// If is null, the token is also considered expired.
82 | ///
83 | public bool IsExpired()
84 | {
85 | return string.IsNullOrEmpty(accessToken) || expiresAt == null || expiresAt < DateTime.UtcNow;
86 | }
87 |
88 | ///
89 | /// Returns true if the refresh token is exist.
90 | ///
91 | public bool HasRefreshToken()
92 | {
93 | return !string.IsNullOrEmpty(refreshToken);
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AccessTokenResponseExtensions.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace Cdm.Authentication.OAuth2
3 | {
4 | public static class AccessTokenResponseExtensions
5 | {
6 | public static bool IsNullOrExpired(this AccessTokenResponse accessTokenResponse)
7 | {
8 | return accessTokenResponse == null || accessTokenResponse.IsExpired();
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthenticationError.cs:
--------------------------------------------------------------------------------
1 | namespace Cdm.Authentication.OAuth2
2 | {
3 | public enum AuthenticationError
4 | {
5 | Other = 0,
6 | Cancelled = 1,
7 | Timeout = 2
8 | }
9 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthenticationException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Cdm.Authentication.OAuth2
4 | {
5 | public class AuthenticationException : Exception
6 | {
7 | public AuthenticationError error { get; }
8 |
9 | public AuthenticationException(AuthenticationError error)
10 | {
11 | this.error = error;
12 | }
13 |
14 | public AuthenticationException(AuthenticationError error, string message) : base(message)
15 | {
16 | this.error = error;
17 | }
18 |
19 | public AuthenticationException(AuthenticationError error, string message, Exception innerException)
20 | : base(message, innerException)
21 | {
22 | this.error = error;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthorizationCodeFlowWithPkce.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Security.Cryptography;
4 | using System.Text;
5 |
6 | namespace Cdm.Authentication.OAuth2
7 | {
8 | ///
9 | /// OAuth 2.0 'Authorization Code' flow with PKCE (Proof Key for Code Exchange).
10 | ///
11 | /// PKCE (RFC 7636) is an extension to the Authorization Code flow to prevent CSRF and authorization code injection
12 | /// attacks. PKCE is not a form of client authentication, and PKCE is not a replacement for a client secret or
13 | /// other client authentication. PKCE is recommended even if a client is using a client secret or other form
14 | /// of client authentication like private_key_jwt.
15 | ///
16 | /// https://www.rfc-editor.org/rfc/rfc7636
17 | ///
18 | public abstract class AuthorizationCodeFlowWithPkce : AuthorizationCodeFlow
19 | {
20 | private string _codeVerifier;
21 |
22 | protected AuthorizationCodeFlowWithPkce(Configuration configuration) : base(configuration)
23 | {
24 | }
25 |
26 | protected override Dictionary GetAuthorizationUrlParameters()
27 | {
28 | var parameters = base.GetAuthorizationUrlParameters();
29 |
30 | _codeVerifier = GenerateRandomDataBase64url(32);
31 | var codeChallenge = Base64UrlEncodeNoPadding(Sha256Ascii(_codeVerifier));
32 |
33 | parameters.Add("code_challenge", codeChallenge);
34 | parameters.Add("code_challenge_method", "S256");
35 |
36 | return parameters;
37 | }
38 |
39 | protected override Dictionary GetAccessTokenParameters(string code)
40 | {
41 | var parameters = base.GetAccessTokenParameters(code);
42 | parameters.Add("code_verifier", _codeVerifier);
43 | return parameters;
44 | }
45 |
46 | private static string GenerateRandomDataBase64url(uint length)
47 | {
48 | var rng = new RNGCryptoServiceProvider();
49 | var bytes = new byte[length];
50 | rng.GetBytes(bytes);
51 | return Base64UrlEncodeNoPadding(bytes);
52 | }
53 |
54 | ///
55 | /// Base64url no-padding encodes the given input buffer.
56 | ///
57 | ///
58 | ///
59 | private static string Base64UrlEncodeNoPadding(byte[] buffer)
60 | {
61 | var base64 = Convert.ToBase64String(buffer);
62 |
63 | // Converts base64 to base64url.
64 | base64 = base64.Replace("+", "-");
65 | base64 = base64.Replace("/", "_");
66 | // Strips padding.
67 | base64 = base64.Replace("=", "");
68 |
69 | return base64;
70 | }
71 |
72 | ///
73 | /// Returns the SHA256 hash of the input string, which is assumed to be ASCII.
74 | ///
75 | private static byte[] Sha256Ascii(string text)
76 | {
77 | var bytes = Encoding.ASCII.GetBytes(text);
78 | using (SHA256Managed sha256 = new SHA256Managed())
79 | {
80 | return sha256.ComputeHash(bytes);
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthorizationCodeRequest.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using UnityEngine.Scripting;
3 |
4 | namespace Cdm.Authentication.OAuth2
5 | {
6 | ///
7 | /// OAuth 2.0 request for an access token using an authorization code as specified in
8 | /// http://tools.ietf.org/html/rfc6749#section-4.1.1.
9 | ///
10 | [Preserve]
11 | [DataContract]
12 | public class AuthorizationCodeRequest
13 | {
14 | ///
15 | /// Gets the response type which is the 'code'.
16 | ///
17 | [Preserve]
18 | [DataMember(Name = "response_type", IsRequired = true)]
19 | public string responseType => "code";
20 |
21 | ///
22 | /// Gets or sets the client identifier as specified in https://www.rfc-editor.org/rfc/rfc6749#section-2.2.
23 | ///
24 | [Preserve]
25 | [DataMember(Name = "client_id", IsRequired = true)]
26 | public string clientId { get; set; }
27 |
28 | ///
29 | /// Gets or sets the URI that the authorization server directs the resource owner's user-agent back to the
30 | /// client after a successful authorization grant, as specified in
31 | /// http://tools.ietf.org/html/rfc6749#section-3.1.2 or null for none.
32 | ///
33 | [Preserve]
34 | [DataMember(Name = "redirect_uri")]
35 | public string redirectUri { get; set; }
36 |
37 | ///
38 | /// Gets or sets space-separated list of scopes, as specified in
39 | /// http://tools.ietf.org/html/rfc6749#section-3.3 or null for none.
40 | ///
41 | [Preserve]
42 | [DataMember(Name = "scope")]
43 | public string scope { get; set; }
44 |
45 | ///
46 | /// Gets or sets the state (an opaque value used by the client to maintain state between the request and
47 | /// callback, as mentioned in http://tools.ietf.org/html/rfc6749#section-3.1.2.2 or null for none.
48 | ///
49 | [Preserve]
50 | [DataMember(Name = "state")]
51 | public string state { get; set; }
52 | }
53 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthorizationCodeRequestError.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using UnityEngine.Scripting;
3 |
4 | namespace Cdm.Authentication.OAuth2
5 | {
6 | [Preserve]
7 | [DataContract]
8 | public class AuthorizationCodeRequestError
9 | {
10 | [Preserve]
11 | [DataMember(IsRequired = true, Name = "error")]
12 | public AuthorizationCodeRequestErrorCode code { get; set; }
13 |
14 | ///
15 | /// OPTIONAL. Human-readable ASCII [USASCII]
16 | /// text providing additional information, used to assist the client developer in understanding
17 | /// the error that occurred.
18 | ///
19 | [Preserve]
20 | [DataMember(Name = "error_description")]
21 | public string description { get; set; }
22 |
23 | ///
24 | /// OPTIONAL. A URI identifying a human-readable web page with information about the error, used to provide
25 | /// the client developer with additional information about the error.
26 | ///
27 | [Preserve]
28 | [DataMember(Name = "error_uri")]
29 | public string uri { get; set; }
30 |
31 | ///
32 | /// REQUIRED if a "state" parameter was present in the client authorization request. The exact value received
33 | /// from the client.
34 | ///
35 | [Preserve]
36 | [DataMember(Name = "state")]
37 | public string state { get; set; }
38 | }
39 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthorizationCodeRequestErrorCode.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using Newtonsoft.Json;
3 | using Newtonsoft.Json.Converters;
4 | using UnityEngine.Scripting;
5 |
6 | namespace Cdm.Authentication.OAuth2
7 | {
8 | ///
9 | ///
10 | ///
11 | [Preserve]
12 | [JsonConverter(typeof(StringEnumConverter))]
13 | [DataContract]
14 | public enum AuthorizationCodeRequestErrorCode
15 | {
16 | ///
17 | /// The request is missing a required parameter, includes an invalid parameter value, includes a parameter
18 | /// more than once, or is otherwise malformed.
19 | ///
20 | [Preserve]
21 | [EnumMember(Value = "invalid_request")]
22 | InvalidRequest,
23 |
24 | ///
25 | /// The client is not authorized to request an authorization code using this method.
26 | ///
27 | [Preserve]
28 | [EnumMember(Value = "unauthorized_client")]
29 | UnauthorizedClient,
30 |
31 | ///
32 | /// The resource owner or authorization server denied the request.
33 | ///
34 | [Preserve]
35 | [EnumMember(Value = "access_denied")]
36 | AccessDenied,
37 |
38 | ///
39 | /// The authorization server does not support obtaining an authorization code using this method.
40 | ///
41 | [Preserve]
42 | [EnumMember(Value = "unsupported_response_type")]
43 | UnsupportedResponseType,
44 |
45 | ///
46 | /// The requested scope is invalid, unknown, or malformed.
47 | ///
48 | [Preserve]
49 | [EnumMember(Value = "invalid_scope")]
50 | InvalidScope,
51 |
52 | ///
53 | /// The authorization server encountered an unexpected condition that prevented it from fulfilling the request.
54 | /// (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to
55 | /// the client via an HTTP redirect.)
56 | ///
57 | [Preserve]
58 | [EnumMember(Value = "server_error")]
59 | ServerError,
60 |
61 | ///
62 | /// The authorization server is currently unable to handle the request due to a temporary overloading or
63 | /// maintenance of the server. (This error code is needed because a 503 Service Unavailable HTTP status code
64 | /// cannot be returned to the client via an HTTP redirect.)
65 | ///
66 | [Preserve]
67 | [EnumMember(Value = "temporarily_unavailable")]
68 | TemporarilyUnavailable
69 | }
70 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthorizationCodeRequestException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Cdm.Authentication.OAuth2
4 | {
5 | public class AuthorizationCodeRequestException : Exception
6 | {
7 | public AuthorizationCodeRequestError error { get; }
8 |
9 | public AuthorizationCodeRequestException(AuthorizationCodeRequestError error)
10 | {
11 | this.error = error;
12 | }
13 |
14 | public AuthorizationCodeRequestException(AuthorizationCodeRequestError error, string message) : base(message)
15 | {
16 | this.error = error;
17 | }
18 |
19 | public AuthorizationCodeRequestException(AuthorizationCodeRequestError error, string message, Exception innerException)
20 | : base(message, innerException)
21 | {
22 | this.error = error;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/AuthorizationCodeResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using UnityEngine.Scripting;
3 |
4 | namespace Cdm.Authentication.OAuth2
5 | {
6 | ///
7 | /// If the resource owner grants the access request, the authorization server issues an authorization code and
8 | /// delivers it to the client by adding the following parameters to the query component of the redirection URI
9 | /// using the "application/x-www-form-urlencoded" format,
10 | /// per Appendix B.
11 | ///
12 | [Preserve]
13 | [DataContract]
14 | public class AuthorizationCodeResponse
15 | {
16 | ///
17 | /// Gets or sets the authorization code received from the authorization server.
18 | ///
19 | [Preserve]
20 | [DataMember(IsRequired = true, Name = "code")]
21 | public string code { get; set; }
22 |
23 | ///
24 | /// The exact value received from the client while making the authorization request as specified in
25 | /// .
26 | ///
27 | [Preserve]
28 | [DataMember(Name = "state")]
29 | public string state { get; set; }
30 | }
31 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/OAuth2/RefreshTokenRequest.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using UnityEngine.Scripting;
3 |
4 | namespace Cdm.Authentication.OAuth2
5 | {
6 | ///
7 | /// OAuth 2.0 request to refresh an access token using a refresh token as specified in
8 | /// http://tools.ietf.org/html/rfc6749#section-6.
9 | ///
10 | [Preserve]
11 | [DataContract]
12 | public class RefreshTokenRequest
13 | {
14 | ///
15 | /// The grant type as 'refresh_token'.
16 | ///
17 | [Preserve]
18 | [DataMember(IsRequired = true, Name = "grant_type")]
19 | public string grantType => "refresh_token";
20 |
21 | ///
22 | /// REQUIRED. The refresh token issued to the client.
23 | ///
24 | [Preserve]
25 | [DataMember(IsRequired = true, Name = "refresh_token")]
26 | public string refreshToken { get; set; }
27 |
28 | ///
29 | /// OPTIONAL. The scope of the access request as described by Section 3.3. The requested scope MUST NOT
30 | /// include any scope not originally granted by the resource owner, and if omitted is treated as equal to
31 | /// the scope originally granted by the resource owner.
32 | ///
33 | [Preserve]
34 | [DataMember(Name = "scope")]
35 | public string scope { get; set; }
36 | }
37 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Utils/JsonHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.Specialized;
3 | using System.Linq;
4 | using Newtonsoft.Json;
5 | using Newtonsoft.Json.Linq;
6 |
7 | namespace Cdm.Authentication.Utils
8 | {
9 | public static class JsonHelper
10 | {
11 | public static Dictionary ToDictionary(object obj)
12 | {
13 | var dictionary = JObject.FromObject(obj).ToObject>();
14 |
15 | if (dictionary != null)
16 | {
17 | // Remove empty parameters.
18 | var keys = dictionary.Keys.Where(key => string.IsNullOrEmpty(dictionary[key])).ToArray();
19 | foreach (var key in keys)
20 | {
21 | dictionary.Remove(key);
22 | }
23 | }
24 |
25 | return dictionary;
26 | }
27 |
28 | public static T FromDictionary(Dictionary dictionary)
29 | {
30 | return JObject.FromObject(dictionary).ToObject();
31 | }
32 |
33 | public static bool TryGetFromDictionary(Dictionary dictionary, out T value)
34 | {
35 | try
36 | {
37 | value = FromDictionary(dictionary);
38 | return true;
39 | }
40 | catch (JsonSerializationException)
41 | {
42 | // ignored
43 | }
44 |
45 | value = default;
46 | return false;
47 | }
48 |
49 | public static bool TryGetFromNameValueCollection(NameValueCollection collection, out T value)
50 | {
51 | var dictionary = new Dictionary();
52 |
53 | foreach (string s in collection)
54 | {
55 | dictionary.Add(s, collection[s]);
56 | }
57 |
58 | return TryGetFromDictionary(dictionary, out value);
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/Utils/UrlBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Specialized;
4 | using System.Web;
5 |
6 | namespace Cdm.Authentication.Utils
7 | {
8 | public class UrlBuilder
9 | {
10 | private readonly UriBuilder _uriBuilder;
11 | private readonly NameValueCollection _query;
12 |
13 | private UrlBuilder(string url)
14 | {
15 | _uriBuilder = new UriBuilder(url);
16 | _query = HttpUtility.ParseQueryString("");
17 | }
18 |
19 | public static UrlBuilder New(string url)
20 | {
21 | return new UrlBuilder(url);
22 | }
23 |
24 | public UrlBuilder SetQueryParameters(Dictionary parameters)
25 | {
26 | foreach (var p in parameters)
27 | {
28 | _query.Set(p.Key, p.Value);
29 | }
30 |
31 | return this;
32 | }
33 |
34 | public override string ToString()
35 | {
36 | _uriBuilder.Query = _query.ToString();
37 | return _uriBuilder.Uri.ToString();
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/Runtime/csc.rsp:
--------------------------------------------------------------------------------
1 | -r:System.Net.Http.dll
2 | -r:System.Web.dll
--------------------------------------------------------------------------------
/Src/com.cdm.authentication/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.cdm.authentication",
3 | "displayName": "Authentication for Unity",
4 | "version": "1.2.0",
5 | "unity": "2021.3",
6 | "author": "CDM Vision",
7 | "description": "Simple OAuth2 client for Unity.",
8 | "hideInEditor": false,
9 | "dependencies": {
10 | "com.unity.nuget.newtonsoft-json": "2.0.2"
11 | }
12 | }
--------------------------------------------------------------------------------