├── .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 | } --------------------------------------------------------------------------------