├── .gitignore ├── .gitmodules ├── 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 ├── EmbeddedCortexClientWin.cs ├── EmotivCortexLib.cs ├── EmotivCortexLibPINVOKE.cs └── ResponseHandlerCpp.cs ├── CortexClient.cs ├── DataBuffer.cs ├── DataStreamManager.cs ├── DataStreamProcess.cs ├── DevDataBuffer.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 | -------------------------------------------------------------------------------- /Documentation/Images/CodeStructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Emotiv/unity-plugin/3f454c125f7446a2cf878cf395d3ab73bc62f705/Documentation/Images/CodeStructure.png -------------------------------------------------------------------------------- /Documentation/ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## Version 3.7 (November 2023) 4 | ### Added 5 | - Support new headset refreshing flow where App need to to call ScanHeadsets() to start headset scanning. 6 | 7 | ## Version 2.7 2(10 July 2021) 8 | ### Added 9 | - Support injectMarker and updateMarker to EEG data stream. 10 | 11 | ## Version 2.7 0(17 Apr 2021) 12 | 13 | ### Added 14 | - Support data parsing for new channel BatteryPercent of "dev" stream which is new from Cortex version 2.7.0. 15 | 16 | ### Fixed 17 | - Fixed issue parsing "Markers" channels from eeg data stream. Actually, we exclude "Markers" data from data buffer 18 | - Fixed issue sometime can not add new method \_methodForRequestId map at CortexClient.cs 19 | 20 | ## Version 2.4 (12 May 2020) 21 | For the moment the following features are supported: 22 | - Subscribe to all data streams: EEG, Motion, Device information, Band power, detections, etc. 23 | - Create a record and stop a record 24 | - Create, load and unload profiles 25 | - Perform Mental Commands and Facial Expression training 26 | 27 | -------------------------------------------------------------------------------- /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 Unity Plugin 2 | 3 | This plugin enables Unity applications to work with the Emotiv Cortex Service (Cortex) for EEG headset integration, data streaming, training, and recording. 4 | 5 | --- 6 | 7 | ## Prerequisites 8 | 9 | 1. [Download and install](https://www.emotiv.com/developer/) the EMOTIV Launcher with Cortex service (Windows/macOS). 10 | 2. Install Unity from [unity3d.com](https://unity3d.com/get-unity/download). 11 | 12 | --- 13 | 14 | ## Setup 15 | 16 | Clone the repo manually or add as a submodule: 17 | ```sh 18 | git submodule add https://github.com/Emotiv/unity-plugin.git /Assets/Plugins/Emotiv-Unity-Plugin 19 | ``` 20 | See the [Unity example](https://github.com/Emotiv/cortex-v2-example/tree/master/unity) for reference. 21 | 22 | --- 23 | 24 | ## Platform Support 25 | 26 | The Emotiv Unity Plugin supports: 27 | - **Cortex Service** (Windows/macOS) 28 | - **Embedded Cortex** (internal use) on **Windows**, **iOS**, and **Android** 29 | 30 | --- 31 | 32 | ## Usage Guide 33 | 34 | ### Unified Interface 35 | 36 | **You only need to use [`EmotivUnityItf`](./Src/EmotivUnityItf.cs)** for all Cortex operations. 37 | You do **not** need to interact directly with `DataStreamManager`, `BCITraining`, or `RecordManager` as previous version. 38 | 39 | All public methods and properties are documented in [`EmotivUnityItf.cs`](./Src/EmotivUnityItf.cs). 40 | 41 | --- 42 | 43 | ### Typical Workflow 44 | 45 | **1.Initialize the Plugin** 46 | 47 | Call `Init()` with your app credentials and options: 48 | ```csharp 49 | EmotivUnityPlugin.EmotivUnityItf.Instance.Init( 50 | clientId: "YOUR_CLIENT_ID", 51 | clientSecret: "YOUR_CLIENT_SECRET", 52 | appName: "YOUR_APP_NAME" 53 | allowSaveLogToFile: true, 54 | isDataBufferUsing: true // or false, see Data Streaming section below 55 | // ...other optional parameters 56 | // ...other optional parameters 57 | ); 58 | ``` 59 | 60 | **2.Start Connection and Authorization** 61 | 62 | Call `Start()` to connect to Cortex and authorize: 63 | ```csharp 64 | EmotivUnityPlugin.EmotivUnityItf.Instance.Start(); 65 | ``` 66 | 67 | **3.Query for Available Headsets** 68 | 69 | Call `QueryHeadsets()` to get the list of detected headsets: 70 | ```csharp 71 | EmotivUnityPlugin.EmotivUnityItf.Instance.QueryHeadsets(); 72 | ``` 73 | 74 | **4.Create a Session with a Headset** 75 | 76 | Once you have at least one headset, call `CreateSessionWithHeadset()`: 77 | ```csharp 78 | EmotivUnityPlugin.EmotivUnityItf.Instance.CreateSessionWithHeadset(headsetId); 79 | ``` 80 | - `headsetId` is a string like `"EPOCX-12345"`. 81 | - If you pass an empty string, the first headset in the list will be used. 82 | 83 | **5.Subscribe to Data, Training, and Recording** 84 | 85 | After the session is created, you can: 86 | - **Subscribe to data streams:** 87 | `SubscribeData(listOfStreams)` 88 | - **Start/stop recording:** 89 | `StartRecord(title)`, `StopRecord()` 90 | - **Inject markers:** 91 | `InjectMarker(label, value)` 92 | - **Manage profiles:** 93 | `LoadProfile(profileName)`, `SaveProfile()`, `UnLoadProfile()` 94 | - **Start/stop training:** 95 | `StartMCTraining()`, `AcceptMCTraining()`, etc. 96 | 97 | See [`EmotivUnityItf.cs`](./Src/EmotivUnityItf.cs) for all available methods and detailed documentation. 98 | 99 | --- 100 | 101 | ### Data Streaming Modes 102 | 103 | When calling `Init()`, you can set the `isDataBufferUsing` parameter: 104 | 105 | - **`isDataBufferUsing = false`:** 106 | Subscribed data (EEG, motion, etc.) will be saved to the internal `_messageLog` and can be accessed via the `MessageLog` property. 107 | This is suitable for simple applications or demos. 108 | 109 | - **`isDataBufferUsing = true` (default):** 110 | Data is not automatically buffered. 111 | You must manually retrieve data using functions such as: 112 | - `GetNumberEEGSamples()` 113 | - `GetEEGData(Channel_t chan)` 114 | - `GetNumberMotionSamples()` 115 | - `GetMotionData(Channel_t chan)` 116 | 117 | **Usage Example:** 118 | ```csharp 119 | int n = emotiv.GetNumberEEGSamples(); 120 | if (n > 0) 121 | { 122 | foreach (var chan in emotiv.GetEEGChannels()) 123 | { 124 | double[] data = emotiv.GetEEGData(chan); 125 | // process data 126 | } 127 | } 128 | ``` 129 | Always check that the number of samples is greater than 0 before retrieving data for each channel. 130 | 131 | --- 132 | 133 | 134 | ### Example 135 | 136 | ```csharp 137 | var emotiv = EmotivUnityPlugin.EmotivUnityItf.Instance; 138 | emotiv.Init("clientId", "clientSecret", "appName"); 139 | emotiv.Start(); 140 | // ... should wait for authorize done... 141 | emotiv.QueryHeadsets(); 142 | // ...wait for headset list... 143 | emotiv.CreateSessionWithHeadset("HEADSET_ID"); 144 | // Now you can subscribe to data, train, or record 145 | ``` 146 | 147 | --- 148 | 149 | ## API Reference 150 | 151 | - All public methods and properties are documented in [`EmotivUnityItf.cs`](./Src/EmotivUnityItf.cs). 152 | - Please refer to the source file for up-to-date descriptions and parameter details. 153 | 154 | --- 155 | 156 | ## Additional Resources 157 | 158 | - [Unity Example Project](https://github.com/Emotiv/cortex-v2-example/tree/master/unity) 159 | - [Cortex API Documentation](https://emotiv.gitbook.io/cortex-api/) 160 | 161 | --- 162 | 163 | ## Release Notes 164 | 165 | See [Documentation/ReleaseNotes.md](Documentation/ReleaseNotes.md). 166 | 167 | --- 168 | 169 | ## License 170 | 171 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 172 | -------------------------------------------------------------------------------- /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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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 TmpVersionFileName = "version.ini"; 20 | public static string TmpDataFileName = "data.dat"; 21 | public static string ProfilesDir = "Profiles"; 22 | public static string LogsDir = "UnityLogs"; 23 | public static int QUERY_HEADSET_TIME = 1000; 24 | public static int TIME_CLOSE_STREAMS = 1000; 25 | public static int RETRY_CORTEXSERVICE_TIME = 5000; 26 | public static int WAIT_USERLOGIN_TIME = 5000; 27 | 28 | // If you use an Epoc Flex headset, then you must put your configuration here 29 | // TODO: need detail here 30 | public static string FlexMapping = @"{ 31 | 'CMS':'TP8', 'DRL':'P6', 32 | 'RM':'TP10','RN':'P4','RO':'P8'}"; 33 | 34 | public static void Init( 35 | string clientId, 36 | string clientSecret, 37 | string appName, 38 | bool allowSaveLogAndDataToFile, 39 | string appUrl, 40 | string providerName, 41 | string emotivAppsPath 42 | ) 43 | { 44 | AppClientId = clientId; 45 | AppClientSecret = clientSecret; 46 | AppName = appName; 47 | AppUrl = appUrl; 48 | ProviderName = providerName; 49 | EmotivAppsPath = emotivAppsPath; 50 | 51 | if (allowSaveLogAndDataToFile) 52 | { 53 | // create tmp directory for unity app 54 | string tmpPath = Utils.GetAppTmpPath(appName, providerName); 55 | LogDirectory = Path.Combine(tmpPath, LogsDir); 56 | DataDirectory = Path.Combine(tmpPath, ProfilesDir); 57 | 58 | // Ensure the directories exist 59 | if (!Directory.Exists(LogDirectory)) 60 | { 61 | Directory.CreateDirectory(LogDirectory); 62 | } 63 | 64 | if (!Directory.Exists(DataDirectory)) 65 | { 66 | Directory.CreateDirectory(DataDirectory); 67 | } 68 | } 69 | else 70 | { 71 | LogDirectory = ""; 72 | DataDirectory = ""; 73 | } 74 | 75 | } 76 | } 77 | 78 | public static class DataStreamName 79 | { 80 | public const string DevInfos = "dev"; 81 | public const string EEG = "eeg"; 82 | public const string Motion = "mot"; 83 | public const string PerformanceMetrics = "met"; 84 | public const string BandPower = "pow"; 85 | public const string MentalCommands = "com"; 86 | public const string FacialExpressions = "fac"; 87 | public const string SysEvents = "sys"; // System events of the mental commands and facial expressions 88 | public const string EQ = "eq"; // EEG quality 89 | } 90 | 91 | public static class WarningCode 92 | { 93 | public const int StreamStop = 0; 94 | public const int SessionAutoClosed = 1; 95 | public const int UserLogin = 2; 96 | public const int UserLogout = 3; 97 | public const int ExtenderExportSuccess = 4; 98 | public const int ExtenderExportFailed = 5; 99 | public const int UserNotAcceptLicense = 6; 100 | public const int UserNotHaveAccessRight = 7; 101 | public const int UserRequestAccessRight = 8; 102 | public const int AccessRightGranted = 9; 103 | public const int AccessRightRejected = 10; 104 | public const int CannotDetectOSUSerInfo = 11; 105 | public const int CannotDetectOSUSername = 12; 106 | public const int ProfileLoaded = 13; 107 | public const int ProfileUnloaded = 14; 108 | public const int CortexAutoUnloadProfile = 15; 109 | public const int UserLoginOnAnotherOsUser = 16; 110 | public const int EULAAccepted = 17; 111 | public const int StreamWritingClosed = 18; 112 | public const int CortexIsReady = 23; 113 | public const int UserNotAcceptPrivateEULA = 28; 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 | 131 | } 132 | 133 | public static class DevStreamParams 134 | { 135 | public const string battery = "Battery"; 136 | public const string signal = "Signal"; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /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/EmbeddedCortexClientWin.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 EmbeddedCortexClientWin : global::System.IDisposable { 13 | private global::System.Runtime.InteropServices.HandleRef swigCPtr; 14 | protected bool swigCMemOwn; 15 | 16 | internal EmbeddedCortexClientWin(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(EmbeddedCortexClientWin 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(EmbeddedCortexClientWin 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 | ~EmbeddedCortexClientWin() { 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 EmbeddedCortexClientWin() : 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/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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 _currRecordId; 20 | private string _currMarkerId; 21 | 22 | public static RecordManager Instance { get; } = new RecordManager(); 23 | 24 | // Event 25 | public event EventHandler informStartRecordResult; 26 | public event EventHandler informStopRecordResult; 27 | 28 | public event EventHandler informMarkerResult; 29 | 30 | // Constructor 31 | public RecordManager () 32 | { 33 | _sessionHandler.CreateRecordOK += OnCreateRecordOK; 34 | _sessionHandler.StopRecordOK += OnStopRecordOK; 35 | _ctxClient.InjectMarkerOK += OnInjectMarkerOK; 36 | _ctxClient.UpdateMarkerOK += OnUpdateMarkerOK; 37 | } 38 | 39 | private void OnStopRecordOK(object sender, Record record) 40 | { 41 | UnityEngine.Debug.Log("RecordManager: OnStopRecordOK recordId: " + record.Uuid + 42 | " at: " + record.EndDateTime); 43 | informStopRecordResult(this, record); 44 | _currRecordId = ""; 45 | } 46 | 47 | private void OnCreateRecordOK(object sender, Record record) 48 | { 49 | informStopRecordResult(this, record); 50 | _currRecordId = record.Uuid; 51 | informStartRecordResult(this, record); 52 | } 53 | private void OnInjectMarkerOK(object sender, JObject markerObj) 54 | { 55 | _currMarkerId = markerObj["uuid"].ToString(); 56 | informMarkerResult(this, markerObj); 57 | } 58 | private void OnUpdateMarkerOK(object sender, JObject markerObj) 59 | { 60 | informMarkerResult(this, markerObj); 61 | } 62 | 63 | /// 64 | /// Create a new record. 65 | /// 66 | public void StartRecord(string title, string description = null, 67 | string subjectName = null, List tags= null) 68 | { 69 | lock(_locker) 70 | { 71 | // start record 72 | _sessionHandler.StartRecord(_authorizer.CortexToken, title, description, subjectName, tags); 73 | } 74 | } 75 | 76 | /// 77 | /// Stop a record that was previously started by StartRecord 78 | /// 79 | public void StopRecord() 80 | { 81 | lock(_locker) 82 | { 83 | _sessionHandler.StopRecord(_authorizer.CortexToken); 84 | } 85 | } 86 | // TODO: Update Record 87 | 88 | /// 89 | /// inject marker 90 | /// 91 | public void InjectMarker(string markerLabel, string markerValue) 92 | { 93 | lock(_locker) 94 | { 95 | string cortexToken = _authorizer.CortexToken; 96 | string sessionId = _sessionHandler.SessionId; 97 | 98 | // inject marker 99 | _ctxClient.InjectMarker(cortexToken, sessionId, markerLabel, markerValue, Utils.GetEpochTimeNow()); 100 | } 101 | } 102 | 103 | /// 104 | /// update marker to set the end date time of a marker, turning an "instance" marker into an "interval" marker 105 | /// 106 | public void UpdateMarker() 107 | { 108 | lock(_locker) 109 | { 110 | string cortexToken = _authorizer.CortexToken; 111 | string sessionId = _sessionHandler.SessionId; 112 | 113 | // update marker 114 | _ctxClient.UpdateMarker(cortexToken, sessionId, _currMarkerId, Utils.GetEpochTimeNow()); 115 | } 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Src/RegistryConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.IO; 4 | #if NETFRAMEWORK || NET5_0_OR_GREATER 5 | using Microsoft.Win32; 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 NETFRAMEWORK || NET5_0_OR_GREATER 21 | if (NeedToAddKeys()) AddRegKeys(); 22 | #else 23 | Debug.LogWarning("RegistryConfig is only supported on Windows."); 24 | #endif 25 | } 26 | 27 | private string CustomUriScheme { get; } 28 | #if NETFRAMEWORK || NET5_0_OR_GREATER 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/3f454c125f7446a2cf878cf395d3ab73bc62f705/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/3f454c125f7446a2cf878cf395d3ab73bc62f705/Src/WebSocket4Net.0.15.2/WebSocket4Net.dll -------------------------------------------------------------------------------- /Src/WebsocketCortexClient.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) 2007 James Newton-King 3 | // 4 | // Permission is hereby granted, free of charge, to any person 5 | // obtaining a copy of this software and associated documentation 6 | // files (the "Software"), to deal in the Software without 7 | // restriction, including without limitation the rights to use, 8 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following 11 | // conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | // OTHER DEALINGS IN THE SOFTWARE. 24 | #endregion 25 | 26 | using System; 27 | using System.Threading; 28 | using WebSocket4Net; 29 | using Newtonsoft.Json.Linq; 30 | using System.Collections.Generic; 31 | using System.Collections; 32 | using System.Timers; 33 | 34 | namespace EmotivUnityPlugin 35 | { 36 | /// 37 | /// Represents a simple client for the Cortex service. 38 | /// 39 | public class WebsocketCortexClient : CortexClient 40 | { 41 | const string Url = "wss://localhost:6868"; 42 | static readonly object _locker = new object(); 43 | private Dictionary _methodForRequestId; 44 | 45 | /// 46 | /// Websocket Client. 47 | /// 48 | private WebSocket _wSC; 49 | 50 | /// 51 | /// Timer for connecting to Emotiv Cortex Service 52 | /// 53 | private System.Timers.Timer _wscTimer = null; 54 | 55 | // Private constructor to prevent direct instantiation 56 | public WebsocketCortexClient() { } 57 | 58 | 59 | // override the init method 60 | public override void Init(object context = null) 61 | { 62 | _wSC = new WebSocket(Config.AppUrl); 63 | // Since Emotiv Cortex 3.7.0, the supported SSL Protocol will be TLS1.2 or later 64 | _wSC.Security.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12; 65 | _wSC.Opened += new EventHandler(WebSocketClient_Opened); 66 | _wSC.Error += new EventHandler(WebSocketClient_Error); 67 | _wSC.Closed += WebSocketClient_Closed; 68 | _wSC.MessageReceived += WebSocketClient_MessageReceived; 69 | _wSC.DataReceived += WebSocketClient_DataReceived; 70 | 71 | // open websocket 72 | Open(); 73 | } 74 | 75 | // override the close method 76 | public override void Close() 77 | { 78 | UnityEngine.Debug.Log("Force close websocket client."); 79 | if (_wscTimer != null) { 80 | _wscTimer = null; 81 | } 82 | // stop websocket client 83 | if (_wSC != null) 84 | _wSC.Close(); 85 | } 86 | 87 | 88 | 89 | /// 90 | /// Set up timer for connecting to Emotiv Cortex service 91 | /// 92 | private void SetWSCTimer() { 93 | if (_wscTimer != null) 94 | return; 95 | _wscTimer = new System.Timers.Timer(Config.RETRY_CORTEXSERVICE_TIME); 96 | // Hook up the Elapsed event for the timer. 97 | _wscTimer.Elapsed += OnTimerEvent; 98 | _wscTimer.AutoReset = false; // do not auto reset 99 | _wscTimer.Enabled = true; 100 | } 101 | 102 | /// 103 | /// Handle for _wscTimer timer timeout 104 | // Retry Connect when time out 105 | /// 106 | private void OnTimerEvent(object sender, ElapsedEventArgs e) 107 | { 108 | UnityEngine.Debug.Log("OnTimerEvent: Retry connect to CortexService...."); 109 | RetryConnect(); 110 | } 111 | 112 | private void RetryConnect() { 113 | m_OpenedEvent.Reset(); 114 | if (_wSC == null || (_wSC.State != WebSocketState.None && _wSC.State != WebSocketState.Closed)) 115 | return; 116 | 117 | _wSC.Open(); 118 | } 119 | 120 | private void WebSocketClient_DataReceived(object sender, DataReceivedEventArgs e) 121 | { 122 | // TODO 123 | UnityEngine.Debug.Log("WebSocketClient_DataReceived"); 124 | } 125 | 126 | /// 127 | /// Build a json rpc request and send message via websocket 128 | /// 129 | public override void SendTextMessage(JObject param, string method, bool hasParam = true) 130 | { 131 | lock(_locker) 132 | { 133 | string request = PrepareRequest(method, param, hasParam); 134 | // UnityEngine.Debug.Log("Send " + method); 135 | // UnityEngine.Debug.Log(request.ToString()); 136 | 137 | // send the json message 138 | _wSC.Send(request); 139 | } 140 | } 141 | 142 | /// 143 | /// Handle message received return from Emotiv Cortex Service 144 | /// 145 | private void WebSocketClient_MessageReceived(object sender, MessageReceivedEventArgs e) 146 | { 147 | OnMessageReceived(e.Message); 148 | } 149 | 150 | /// 151 | /// Handle when socket close 152 | /// 153 | private void WebSocketClient_Closed(object sender, EventArgs e) 154 | { 155 | OnWSConnected(false); 156 | // start connecting cortex service again 157 | if (_wscTimer != null) 158 | _wscTimer.Start(); 159 | } 160 | 161 | /// 162 | /// Handle when socket open 163 | /// 164 | private void WebSocketClient_Opened(object sender, EventArgs e) 165 | { 166 | m_OpenedEvent.Set(); 167 | if (_wSC.State == WebSocketState.Open) { 168 | OnWSConnected(true); 169 | // stop timer 170 | _wscTimer.Stop(); 171 | 172 | } else { 173 | UnityEngine.Debug.Log("Open Websocket unsuccessfully."); 174 | } 175 | } 176 | 177 | /// 178 | /// Handle error when try to open socket 179 | /// 180 | private void WebSocketClient_Error(object sender, SuperSocket.ClientEngine.ErrorEventArgs e) 181 | { 182 | UnityEngine.Debug.Log(e.Exception.GetType() + ":" + e.Exception.Message + Environment.NewLine + e.Exception.StackTrace); 183 | 184 | if (e.Exception.InnerException != null) { 185 | UnityEngine.Debug.Log(e.Exception.InnerException.GetType()); 186 | OnWSConnected(false); 187 | // start connecting cortex service again 188 | _wscTimer.Start(); 189 | } 190 | } 191 | 192 | /// 193 | /// Open a websocket client. 194 | /// 195 | private void Open() 196 | { 197 | // set timer for connect cortex service 198 | SetWSCTimer(); 199 | //Open websocket 200 | m_OpenedEvent.Reset(); 201 | if (_wSC == null || (_wSC.State != WebSocketState.None && _wSC.State != WebSocketState.Closed)) 202 | return; 203 | 204 | _wSC.Open(); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------